on d86146b help sign in
CLAUDE.md+6−12
since splitsnapshots
unifiedsplit
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
4
4
5## What this is
5## What this is
6
6
7−`j` is a CLI for Jujutsu (jj) repositories whose interface is a small, pure, dynamically typed functional language. A repository is a value (`Repo`, a zipper over the commit tree), a command is a function `Repo -> Repo`, and the whole user-facing vocabulary (`new`, `describe`, `squash`, `tree`, …) is written *in the language* in `config.j`. The Rust binary supplies only the language, the builtins `config.j` declares, eleven reserved commands, and the bridge to jj's on-disk format via `jj-lib` (no `jj` binary needed at runtime).
7+`j` is a CLI for Jujutsu (jj) repositories whose interface is a small, pure, dynamically typed functional language. A repository is a value (`Repo`, a zipper over the commit tree), a command is a function `Repo -> Repo`, and the whole user-facing vocabulary (`new`, `describe`, `squash`, `tree`, …) is written *in the language* in `config.j`. The Rust binary supplies only the language, the builtins `config.j` declares, eight reserved commands, and the bridge to jj's on-disk format via `jj-lib` (no `jj` binary needed at runtime).
8
8
9## Commands
9## Commands
10
10
@@ -21,40 +21,34 @@ There is no devShell in `flake.nix`; `cargo`/`rustc` come from the system. The N
21- `tests/cli.rs`, `tests/remote.rs`, `tests/interop.rs` run the built binary (`CARGO_BIN_EXE_j`) against real repos in temp dirs, each with its own `XDG_CONFIG_HOME`. They need `git` on `PATH`; `interop.rs` also uses the real `jj` binary and skips itself if it is absent.
21- `tests/cli.rs`, `tests/remote.rs`, `tests/interop.rs` run the built binary (`CARGO_BIN_EXE_j`) against real repos in temp dirs, each with its own `XDG_CONFIG_HOME`. They need `git` on `PATH`; `interop.rs` also uses the real `jj` binary and skips itself if it is absent.
22- Everything else runs the interpreter over the in-memory `MemBackend` — no repository involved.
22- Everything else runs the interpreter over the in-memory `MemBackend` — no repository involved.
23- `tests/laws.rs` and `tests/showprop.rs` are proptest suites; their `*.proptest-regressions` files are checked in on purpose.
23- `tests/laws.rs` and `tests/showprop.rs` are proptest suites; their `*.proptest-regressions` files are checked in on purpose.
24−- `difft` optionally enhances `review`/`difft`; an absent executable falls back to unified diffs with path headers (binary summaries for non-text content). Other launch errors still fail. `J_GIT` overrides the `git` executable used for fetch.
24+- `difft` is only needed at runtime for `review`/`difft`. `J_GIT` overrides the `git` executable used for fetch.
25
25
26## The spec is normative
26## The spec is normative
27
27
28`specs/main.md` is the specification the implementation was written from, and the code cites it everywhere as `§N.M` (e.g. `§7.5` = persistence). When changing behaviour, find the section first, and keep code, spec, and tests agreeing. `specs/tree.md` is a newer, complete replacement for `§7.11` (the "rails" tree layout) and is what `render.rs` implements.
28`specs/main.md` is the specification the implementation was written from, and the code cites it everywhere as `§N.M` (e.g. `§7.5` = persistence). When changing behaviour, find the section first, and keep code, spec, and tests agreeing. `specs/tree.md` is a newer, complete replacement for `§7.11` (the "rails" tree layout) and is what `render.rs` implements.
29
29
30+Known drift to be aware of (not to copy):
31+- `README.md` links to `SPEC.md`; the file is `specs/main.md`.
30−`§9` of the spec is a verbatim copy of `config.j`; keep it synchronized when the reference vocabulary changes. **`config.j` is the source of truth** for runnable definitions, and `docs/base.md` documents the same vocabulary.
32+- `§9` of the spec is a verbatim copy of `config.j`, but `config.j` is ahead of it (`touchedPaths`, the extra `treeWith` options, `treeCompact`, `treeData`). **`config.j` is the source of truth**; `docs/base.md` lags the same way.
31
33
32## Architecture
34## Architecture
33
35
34−One run of `j EXPR` (`src/main.rs::run_expression`, mirroring spec §1.2): load + validate `config.j` → open the repo and take the lock → parse the expression → build the `Repo` value, snapshotting the working directory → resolve `@id` literals → evaluate the config's definitions, then the expression → if the result is a function, apply it to the repo **once** → if the final value is a `Repo`, persist only if it differs from the one loaded; display other values without persisting. Exit codes: 1 crash, 2 usage/no repo/locked, 3 config or parse error. All work happens on a thread with a 512 MB stack (`main`).
36+One run of `j EXPR` (`src/main.rs::run_expression`, mirroring spec §1.2): load + validate `config.j` → open the repo and take the lock → parse the expression → build the `Repo` value, snapshotting the working directory → resolve `@id` literals → evaluate the config's definitions, then the expression → if the result is a function, apply it to the repo **once** → if the final value is a `Repo` that differs from the one loaded, persist it; otherwise display it and persist nothing. Exit codes: 1 crash, 2 usage/no repo/locked, 3 config or parse error. All work happens on a thread with a 512 MB stack (`main`).
35
37
36Language pipeline, all in `src/`:
38Language pipeline, all in `src/`:
37- `lex.rs` → `parse.rs` → `ast.rs`: the parser implements the two layout rules and needs the set of global names (`Config::global_names`) so expressions cannot shadow them.
39- `lex.rs` → `parse.rs` → `ast.rs`: the parser implements the two layout rules and needs the set of global names (`Config::global_names`) so expressions cannot shadow them.
38- `eval.rs`: strict, call-by-value, **trampolined** CEK-style machine (`State`/`Cont`) so deep recursion in user code cannot overflow the native stack. Keep new evaluation paths on the trampoline rather than recursing in Rust.
40- `eval.rs`: strict, call-by-value, **trampolined** CEK-style machine (`State`/`Cont`) so deep recursion in user code cannot overflow the native stack. Keep new evaluation paths on the trampoline rather than recursing in Rust.
39- `value.rs`: `Value`. Two kinds of laziness exist purely for large-repo performance and must stay invisible to the language: `Value::Thunk` (a commit's `files` list, forced by `Value::field`/`value_eq`) and `BlobContent::Lazy` (file bytes; lazy blobs compare by content id without being read). Prefer comparing ids/tree hashes over forcing content.
41- `value.rs`: `Value`. Two kinds of laziness exist purely for large-repo performance and must stay invisible to the language: `Value::Thunk` (a commit's `files` list, forced by `Value::field`/`value_eq`) and `BlobContent::Lazy` (file bytes; lazy blobs compare by content id without being read). Prefer comparing ids/tree hashes over forcing content.
40− Recursive `let` environments can form ownership cycles through their closures. The cycle collector traces their shared values while preserving externally held values, and runs during scope allocation, after evaluation, and when an interpreter is dropped. Keep its ownership traversal in sync with changes to values and function wrappers; `tests/env_lifetimes.rs` covers reclamation and escaping closures.
41- `shape.rs`: shapes from `config.j` type declarations, and runtime **contracts** compiled from signatures — checked at every call, one level deep (kind, list-ness, field names). Records are structural: a value *is* a `Commit` iff its field set matches.
42- `shape.rs`: shapes from `config.j` type declarations, and runtime **contracts** compiled from signatures — checked at every call, one level deep (kind, list-ness, field names). Records are structural: a value *is* a `Commit` iff its field set matches.
42- `builtins.rs`: native implementations of the names `config.j` declares without defining.
43- `builtins.rs`: native implementations of the names `config.j` declares without defining.
43- `config.rs`: parses and validates `config.j`, orders definitions by dependency, resolves `@id` literals. Four definitions are read by the binary itself: `user`, `tree`, `immutable`, `labelled`.
44- `config.rs`: parses and validates `config.j`, orders definitions by dependency, resolves `@id` literals. Four definitions are read by the binary itself: `user`, `tree`, `immutable`, `labelled`.
44
45
45−`help [NAME]` and `commands [QUERY]` are reserved discovery commands. `--help` and `-h` open help as the first word or as separate argv entries after a command, before evaluation; comments within a single expression argument or stdin retain language semantics. Discovery never opens a repository or evaluates config definitions. General help, reserved-command help, and `help config` work without loading config; the custom vocabulary reads config without writing it, using the embedded reference only when no file exists. Keep these paths usable outside a repository and when help is needed to recover from a broken config. Literal reserved-command arguments retain separate shell argument boundaries; a whole command supplied as one string or stdin is whitespace-split.
46−
47−Identity validation rejects blank and shipped placeholder values at init, clone, and commit-authoring boundaries. Inspections remain usable without a configured identity. Push never records the working directory: it warns about unrecorded edits before sending recorded content. Undo/redo refuse dirty working copies and explain that `j id` adds an operation before the previously intended undo. Interactive success messages report saved/no-change status and focus, setup identity, or the affected undo/redo operation; plain terminals keep confirmations, while redirected output and CI remain quiet on success.
48−
49Repository side:
46Repository side:
50−- `setup.rs` (binary): explicit first-run configuration, optionally with a supplied identity, using exclusive atomic publication. It never replaces an existing config or opens a repository.
51- `domain.rs`: the `Backend` trait — everything repository-shaped the interpreter needs — plus `MemBackend` (the test double required by spec §10) and snapshot helpers shared by both backends.
47- `domain.rs`: the `Backend` trait — everything repository-shaped the interpreter needs — plus `MemBackend` (the test double required by spec §10) and snapshot helpers shared by both backends.
52- `jj.rs`: `JjBackend` on `jj-lib`/`gix`: locating the repo, building the `Repo` value, `persist`, the lock, and the reserved commands (`init`, `clone`, `remote`, `fetch`, `push`, `undo`, `redo`, `ops`). Reserved commands are matched on the first word in `main.rs` before any parsing and are not part of the language.
48- `jj.rs`: `JjBackend` on `jj-lib`/`gix`: locating the repo, building the `Repo` value, `persist`, the lock, and the reserved commands (`init`, `clone`, `remote`, `fetch`, `push`, `undo`, `redo`, `ops`). Reserved commands are matched on the first word in `main.rs` before any parsing and are not part of the language.
53- `repo.rs`: zipper navigation (`by`) and the persistence validation shared by the `validate` builtin and `JjBackend::persist`, which is what makes `tree . validate . EDIT` an honest dry run.
49- `repo.rs`: zipper navigation (`by`) and the persistence validation shared by the `validate` builtin and `JjBackend::persist`, which is what makes `tree . validate . EDIT` an honest dry run.
54- `render.rs`: value display (§5.1) and tree rendering. `show.rs`: `show` (must round-trip through the parser — property-tested), unified diff, and the `difft` subprocess bridge.
50- `render.rs`: value display (§5.1) and tree rendering. `show.rs`: `show` (must round-trip through the parser — property-tested), unified diff, and the `difft` subprocess bridge.
55
51
56−The reference `save` is `id`; `preview edit` is `tree . validate . edit`. Validation alone still returns a `Repo` and can persist changes. `unrecorded : Repo -> [Path]` is a native inspection of the invocation's loaded/snapshotted files, independent of later expression edits; status exposes it alongside `changed` (displayed as `in commit`). Help's default catalog shows everyday tasks and a few custom definitions; `commands all` shows every configured name, and queries recognize common Git/task synonyms without presenting unavailable config names as commands. A push with no selected bookmarks records no operation and reports that nothing was published; nonempty pushes report published/deleted/unchanged counts interactively. Saved edits report conflict recovery guidance even when output is redirected.
57−
58### `config.j` is code, and it is embedded
52### `config.j` is code, and it is embedded
59
53
60`config.j` is `include_str!`'d into the binary (written to `~/.config/j/config.j` only when no user config exists) and into most test files, so editing it rebuilds both and every test exercises the reference config unmodified. An existing user config is never updated: a change that the binary depends on (a new builtin declaration, a new required `treeWith` option) breaks existing user configs until they are edited by hand.
54`config.j` is `include_str!`'d into the binary (written to `~/.config/j/config.j` only when no user config exists) and into most test files, so editing it rebuilds both and every test exercises the reference config unmodified. An existing user config is never updated: a change that the binary depends on (a new builtin declaration, a new required `treeWith` option) breaks existing user configs until they are edited by hand.
README.md+37−185
@@ -27,52 +27,6 @@ The whole vocabulary — `new`, `describe`, `squash`, `tree`, `rebase`, … —
27written *in the language* in `config.j`, and you extend it by writing more
27written *in the language* in `config.j`, and you extend it by writing more
28functions.
28functions.
29
29
30−## Find a command
31−
32−```
33−j help -- a quick start, grouped commands, and examples
34−j commands -- common tasks and how to do them
35−j commands all -- the full vocabulary in your config
36−j commands rebase -- search names, signatures, and descriptions
37−j help describe -- see a command's signature, documentation, and examples
38−j help push -- learn a reserved command's usage
39−j help config -- find your config and set your identity
40−```
41−
42−`j --help` and `j -h` also open help. A separate trailing help argument, such
43−as `j abandon --help`, opens that command's help without executing it.
44−Help and command discovery work outside a repository and never change your
45−files or configuration. The catalog includes
46−your own definitions, even ones without a signature; when no config exists, it
47−uses the embedded reference config. General help and help for reserved commands
48−are available even if your config is broken.
49−
50−Set up with your own name and email, then choose one starting point:
51−
52−```sh
53−j setup 'Ada Lovelace' ada@example.org
54−
55−# Start a new project
56−mkdir project && cd project
57−j init
58−```
59−
60−```sh
61−# Or join an existing project
62−j clone URL project
63−cd project
64−```
65−
66−`init` and `clone` leave you on a working-copy commit: the commit you are
67−editing now. Edit files, then run `j 'describe "first change"'` to save the
68−files and their description. Use `j save` to save files without changing the
69−description. Run `j new` **before** editing the next change. `j status` shows
70−both changes in the commit and edits that still need saving; `j tree` shows
71−where that commit sits in history. Compose commands right to left:
72−`j 'describe "first change" . new'` creates a commit, then describes it.
73−Preview an edit with `j 'preview squash'`. Search familiar tasks with
74−`j commands commit`, `j commands checkout`, or `j commands branch`.
75−
76---
30---
77
31
78## Why `j`?
32## Why `j`?
@@ -93,7 +47,7 @@ Preview an edit with `j 'preview squash'`. Search familiar tasks with
93### With Nix (recommended)
47### With Nix (recommended)
94
48
95The repository provides a flake with one default binary, `j`. Difftastic is a
49The repository provides a flake with one default binary, `j`. Difftastic is a
96−runtime enhancement and is wired in automatically.
50+runtime dependency and is wired in automatically.
97
51
98```
52```
99nix build # builds ./result/bin/j
53nix build # builds ./result/bin/j
@@ -109,70 +63,22 @@ The reference `config.j` is installed alongside the binary
109cargo build --release # produces target/release/j
63cargo build --release # produces target/release/j
110```
64```
111
65
112−Run `j setup` to create the reference `config.j` (see next section).
66+You'll want the reference `config.j` (see next section) and, for `review`/`difft`,
113−[Difftastic](https://difftastic.wilfred.me.uk/) on your `PATH` improves
67+[difftastic](https://difftastic.wilfred.me.uk/) on your `PATH`.
114−`review`/`difft`; without it they use a unified diff.
115
68
116---
69---
117
70
118## Configuration
71## Configuration
119
72
120−### Terminal colors and motion
121−
122−In an interactive terminal, `j` uses vivid branch colors, a blue focus band,
123−colorful data and status messages, and animated progress indicators while
124−repository operations or expressions run. Each indicator names the current
125−operation and shows a spinner, a moving activity bar, and elapsed time. The
126−bar shows activity, not a completion percentage; on narrow terminals it makes
127−room for the operation's name. Colored trees and tables appear in a quick
128−cascade. Progress starts only when an operation takes a moment; fast commands
129−do not wait for a spinner.
130−
131−To keep the colors but turn off animation:
132−
133−```sh
134−J_ANIMATIONS=never j tree
135−```
136−
137−Set `NO_COLOR=1` for plain, motion-free output. Motion is also disabled when
138−either output stream is redirected, `TERM=dumb`, or `CI` is nonempty. Automatic
139−colors follow the terminal for each stream; piped output stays plain unless
140−you explicitly request `treeWith`'s `color = "always"`. `NO_COLOR` takes
141−precedence even over that setting.
142−
143−Interactive edits report `Saved changes` or `No changes`, followed by the
144−focused commit's short id and description. `NO_COLOR=1` and `TERM=dumb` keep
145−these confirmations in plain text. Success confirmations are suppressed when
146−either output stream is redirected or CI is set; warnings and errors still
147−appear.
148−
149−### User config
150−
151−`j setup 'Your actual name' you@your-domain.example` creates the full reference
152−config with your identity. `j setup` without arguments creates an editable
153−default and tells you where to set the identity. Neither form overwrites an
154−existing file; use `j help config` to find and edit it.
155−
156−Expressions read `~/.config/j/config.j` (or `$XDG_CONFIG_HOME/j/config.j`).
73+`j` reads `~/.config/j/config.j` on every run (or `$XDG_CONFIG_HOME/j/config.j`).
157An expression can only use a name this file declares or defines. The reference
74An expression can only use a name this file declares or defines. The reference
158−[`config.j`](config.j) ships with the binary. Commands that execute against a
75+[`config.j`](config.j) ships with the binary — copy it and edit at least the
159−repository create an editable copy there when it is missing; help and discovery
76+identity:
160−use the embedded copy without writing it. Edit at least the identity:
161
77
162```
78```
163−user = { name = "Ada Lovelace", email = "ada@example.org" }
79+user = { name = "Your Name", email = "you@example.com" }
164```
80```
165
81
166−Use your own name and email. `j init`, `j clone`, and edits that save changes
167−reject empty fields and the shipped `Your Name` / `you@example.com`
168−placeholders. Interactive setup confirms the identity it used. `j help config`
169−shows the actual config path and remains available when the file is broken.
170−
171−Existing configurations keep their own vocabulary. To adopt new helpers,
172−copy the `save` and `preview` definitions, the `unrecorded` declaration, and
173−the updated `status` definition from the [reference config](config.j), preserving your
174−identity and custom definitions. `j setup` does not upgrade existing files.
175−
176The reference config defines the entire base vocabulary (see
82The reference config defines the entire base vocabulary (see
177[docs/base.md](docs/base.md)). Four definitions are read by `j` itself: `user`
83[docs/base.md](docs/base.md)). Four definitions are read by `j` itself: `user`
178(who commits), `tree` (how a `Repo` prints), `immutable` (which commits may
84(who commits), `tree` (how a `Repo` prints), `immutable` (which commits may
@@ -182,19 +88,17 @@ never be rewritten), and `labelled` (what a `%name` literal means).
182
88
183## How it works
89## How it works
184
90
185−Every expression run:
91+Every `j` run:
186
92
187−1. Snapshots the working directory for evaluation.
93+1. Snapshots the working directory into the working-copy commit.
188−2. Builds a `Repo` value focused on the working-copy commit.
94+2. Builds a `Repo` value focused on that commit.
1893. Evaluates your expression.
953. Evaluates your expression.
1904. If the result is a function, applies it to the `Repo`.
964. If the result is a function, applies it to the `Repo`.
191−5. If the final value is a `Repo` that changed, persists it. An unchanged
97+5. If the final value is a `Repo` that changed, persists it; otherwise prints
192− `Repo` saves nothing; other values print without saving.
98+ the value and changes nothing.
193
99
194So most of what you write is an **edit** — a function `Repo -> Repo`. A result
100So most of what you write is an **edit** — a function `Repo -> Repo`. A result
195that's plain data is printed and persists nothing (a dry run).
101that's plain data is printed and persists nothing (a dry run).
196−`j status` sees current file edits but does not record them. Use `j save` to
197−record those edits without changing anything else before pushing.
198
102
199```
103```
200j 'length . commits . top' -- a number: how many commits? prints, persists nothing
104j 'length . commits . top' -- a number: how many commits? prints, persists nothing
@@ -239,19 +143,14 @@ Opens a new commit *and* describes it, in one step.
239j 'status'
143j 'status'
240```
144```
241
145
242−A summary of the focus: id, message, labels, changed paths, unrecorded edits,
146+A summary of the focus: id, message, labels, changed paths, conflicted paths.
243−and conflicted paths. `in commit` (the `changed` field) compares the commit
244−with its parent;
245−`unrecorded` compares the current files with their last saved state. After
246−`j save`, a commit can still have changed paths while all edits are recorded.
247
147
248```
148```
249−in commit No changes
149+changed 0 items
250−conflicts No conflicts
150+conflicts 0 items
251−id zrkmkkvrvtokumkuzrkmkkvrvtokumku
151+id zrkmkkvrvtokumkuzrkmkkvrvtokumku
252−labels No labels
152+labels 0 items
253−message add a library
153+message add a library
254−unrecorded All edits recorded
255```
154```
256
155
257```
156```
@@ -291,33 +190,25 @@ The same tree with ages and authors in the margin.
291j 'log'
190j 'log'
292```
191```
293
192
294−A table of every commit, top first, with a `✓` on the focus, short ids, and
193+A table of every commit, top first, with a `✓` on the focus.
295−label names. Tables fit the terminal width, switching to fields on separate
296−lines on very narrow terminals. Redirected output preserves full cell text.
297
194
298```
195```
299j 'review'
196j 'review'
300```
197```
301
198
302−The focus's changes as one page of diffs, using difftastic when available
199+The focus's changes, rendered by difftastic as one page of diffs.
303−and a unified diff otherwise.
304
200
305### Move around
201### Move around
306
202
307−Navigation checks out the selected commit's files so you can edit them.
308−
309```
203```
310j 'prev' -- to the parent
204j 'prev' -- to the parent
311j 'next' -- to the only child
205j 'next' -- to the only child
206+j 'top' -- to the top of history
312j 'tip' -- to the tip of the current line
207j 'tip' -- to the tip of the current line
313j 'by @wqzt' -- to a specific commit, by id
208j 'by @wqzt' -- to a specific commit, by id
314−j 'new . goto trunk' -- start a change on top of the protected main line
209+j 'goto %main' -- to what the remote calls main
315```
210```
316
211
317−Use `j 'tree . goto trunk'` to inspect main without moving the working copy.
318−At a fork, use an id from `j tree` with `by`; `next` requires one child.
319−Main must exist on the remote (`j fetch`) before `trunk` can select it.
320−
321### Undo and history of actions
212### Undo and history of actions
322
213
323```
214```
@@ -326,25 +217,18 @@ j redo -- reapply it
326j ops -- the operation log, newest first
217j ops -- the operation log, newest first
327```
218```
328
219
329−Undo and redo require a working copy with no unrecorded edits. If you run
330−`j id` to record edits first, that creates a new operation: the next `j undo`
331−undoes the recording, and another undo reaches the preceding edit. A new
332−recording also clears pending redos. Inspect `j ops` when deciding how far to
333−go back. Interactive confirmations name the affected operation and restored
334−focus.
335−
336### A safe dry run
220### A safe dry run
337
221
338−`preview` validates an edit and returns its resulting tree without saving:
222+Because a `Text` or `Repo`-returning-but-unchanged result persists nothing,
223+you can preview any edit by composing it with `tree` (or `validate`):
339
224
340```
225```
341−j 'preview squash'
226+j 'tree . squash' -- show the tree as squash would leave it
342−j 'preview (rebase trunk)'
227+j 'tree . validate . squash' -- the same, but crash if squash couldn't persist
343```
228```
344
229
345−This is shorthand for `tree . validate . EDIT`. `tree . EDIT` also previews,
230+`validate` returns the repo unchanged if persisting would succeed, and crashes
346−but does not check whether the result can be saved. `validate . EDIT` alone
231+with the exact error otherwise — an honest dry run.
347−**saves** a changed repository; validation is not a dry-run mode.
348
232
349---
233---
350
234
@@ -446,27 +330,6 @@ j 'push (unlabel "old-feature")' -- delete a bookmark
446j 'push relabel' -- after a rewrite, re-point the remote's bookmarks
330j 'push relabel' -- after a rewrite, re-point the remote's bookmarks
447```
331```
448
332
449−Push sends recorded content. If files have changed since the last saved edit,
450−it warns that those changes are omitted, then sends the recorded commits.
451−Run `j save` and push again to include them; `j status` alone does not record
452−changes.
453−
454−For the first publication, choose a bookmark name with
455−`j 'push (label "feature" here)'`. `j push relabel` updates existing bookmarks;
456−it does not create the first one. Push reports when no updates were selected,
457−and gives counts of published, deleted, and unchanged bookmarks for a
458−nonempty selection in an interactive terminal.
459−
460−### Resolve conflicts
461−
462−A history edit can succeed with conflicts. Run `j status` to find conflicted
463−files, inspect them with `j review`, and edit the files to remove the conflict
464−markers and keep the intended content. Run `j save`, then `j status` again
465−to check for `No conflicts` and `All edits recorded`. There is no separate
466−continue command. `j conflicts` lists conflicted commits across history;
467−use `j 'by @id'` to work on another mutable commit when needed, and check
468−`j conflicts` again after resolving it.
469−
470---
333---
471
334
472## Advanced examples
335## Advanced examples
@@ -550,9 +413,9 @@ Rebase if you can; leave the repository alone if it crashes.
550Anything added to `config.j` whose value is an `Edit` is a new command:
413Anything added to `config.j` whose value is an `Edit` is a new command:
551
414
552```
415```
553−-- in config.j: squash into the parent and name the combined commit "release"
416+-- in config.j: fold the whole stack into one commit called "release"
554release : Edit
417release : Edit
555−release = describe "release" . prev . squash
418+release = describe "release" . (contract everything)
556```
419```
557
420
558Then `j release` works like any built-in.
421Then `j release` works like any built-in.
@@ -561,31 +424,20 @@ Then `j release` works like any built-in.
561
424
562## Reserved commands
425## Reserved commands
563
426
564−A handful of commands are built in (not expressions): they provide help,
427+A handful of commands are built in (not expressions): they create or connect
565−create or connect repositories, move refs, and walk the operation log.
428+repositories, move refs, and walk the operation log. None take flags.
566
429
567| command | what it does |
430| command | what it does |
568|---|---|
431|---|---|
569−| `j help [NAME]` | show a quick start or help for a reserved command or config name |
570−| `j commands [QUERY]` | show common tasks, search by task/name, or use `all` for the full vocabulary |
571−| `j setup [NAME EMAIL]` | create the full reference config, optionally with your identity; never overwrite an existing file |
572| `j init` | create a colocated `.jj`/`.git` repository here (adopting an existing `.git`) |
432| `j init` | create a colocated `.jj`/`.git` repository here (adopting an existing `.git`) |
573−| `j clone URL [DIR]` | clone `URL` into `DIR` (default: the repository's name, as `git clone` picks it) |
433+| `j clone URL [DIR]` | clone `URL` into `DIR` (default: last path component, minus `.git`) |
574| `j remote URL` | set `origin`'s URL |
434| `j remote URL` | set `origin`'s URL |
575| `j fetch` | fetch from `origin` |
435| `j fetch` | fetch from `origin` |
576| `j push EXPR` | evaluate `EXPR` to push/drop records and push them |
436| `j push EXPR` | evaluate `EXPR` to push/drop records and push them |
577| `j undo` / `j redo` | walk the operation log |
437| `j undo` / `j redo` | walk the operation log |
578| `j ops` | print the operation log |
438| `j ops` | print the operation log |
579
439
580−`--help` and `-h` open help as the first word or as a separate argument after
581−the command: `j --help describe`, `j describe --help`, and `j describe -h`
582−all open help for `describe`. Inside a single expression argument or stdin,
583−language comments still apply: `j 'new --help'` executes `new`. There are no
584−other general command flags. Everything else is an expression in the language.
440+Everything else is an expression in the language.
585−
586−Quote literal reserved-command arguments separately to keep spaces intact:
587−`j clone URL 'my project'` and `j remote '/path/to/shared repo.git'`. A whole
588−reserved command supplied as one string or on stdin is split on whitespace.
589
441
590---
442---
591
443
@@ -597,7 +449,7 @@ reserved command supplied as one string or on stdin is split on whitespace.
597| **[docs/language.md](docs/language.md)** | The `j` language in detail: values, syntax, semantics, builtins, contracts, `show`, the `Repo` zipper, and many examples. Start here to learn the language. |
449| **[docs/language.md](docs/language.md)** | The `j` language in detail: values, syntax, semantics, builtins, contracts, `show`, the `Repo` zipper, and many examples. Start here to learn the language. |
598| **[docs/base.md](docs/base.md)** | The complete base vocabulary: every builtin and every `config.j` type and function, grouped logically, each with a description and examples, plus an alphabetical index. |
450| **[docs/base.md](docs/base.md)** | The complete base vocabulary: every builtin and every `config.j` type and function, grouped logically, each with a description and examples, plus an alphabetical index. |
599| **[docs/comparison.md](docs/comparison.md)** | A structured three-way comparison of git, jj, and `j`: concept mappings, side-by-side command tables, and a full workflow in each. |
451| **[docs/comparison.md](docs/comparison.md)** | A structured three-way comparison of git, jj, and `j`: concept mappings, side-by-side command tables, and a full workflow in each. |
600−| **[specs/main.md](specs/main.md)** | The normative specification of the language, the CLI, and repository semantics. |
452+| **[SPEC.md](SPEC.md)** | The normative specification of the language, the CLI, and repository semantics. |
601| **[config.j](config.j)** | The reference configuration: the entire base vocabulary, written in the language. |
453| **[config.j](config.j)** | The reference configuration: the entire base vocabulary, written in the language. |
602
454
603---
455---
@@ -605,11 +457,11 @@ reserved command supplied as one string or on stdin is split on whitespace.
605## Project layout
457## Project layout
606
458
607```
459```
608−config.j the reference base vocabulary
460+config.j the reference base vocabulary (read on every run)
609src/ the j implementation (Rust, on jj-lib)
461src/ the j implementation (Rust, on jj-lib)
610tests/ unit, property, and CLI end-to-end tests
462tests/ unit, property, and CLI end-to-end tests
611docs/ language.md, base.md, comparison.md
463docs/ language.md, base.md, comparison.md
612−specs/ the language, CLI, repository, and tree specifications
464+SPEC.md the specification
613flake.nix Nix package: one binary, j, with difftastic as a runtime dep
465flake.nix Nix package: one binary, j, with difftastic as a runtime dep
614```
466```
615
467
config.j+22−107
@@ -76,10 +76,6 @@ Drop = { delete : Label } -- delete the remote's bookmark
76------------------------------------------------------------------------------
76------------------------------------------------------------------------------
77
77
78(.) : (b -> c) -> (a -> b) -> a -> c -- composition, right to left
78(.) : (b -> c) -> (a -> b) -> a -> c -- composition, right to left
79−-- Return a value unchanged. As a repository command, `j id` records the
80−-- snapshotted working-copy edits in the focused commit. `j save` is the
81−-- descriptive alias in this reference config; inspections such as `j status`
82−-- and `j tree` do not record those edits.
83id : a -> a
79id : a -> a
84const : a -> b -> a
80const : a -> b -> a
85crash : Text -> a -- abort the program with a message
81crash : Text -> a -- abort the program with a message
@@ -141,37 +137,24 @@ splitOn : Text -> Text -> [Text] -- splitOn "/" "a/b" = ["a" "b"]
141------------------------------------------------------------------------------
137------------------------------------------------------------------------------
142
138
143-- Replay a change onto a snapshot: "onto, with the change from `from` to `to`
139-- Replay a change onto a snapshot: "onto, with the change from `from` to `to`
144−-- applied." Where the change collides with what is already there, the path's
140+-- applied." Total: where the change collides with what is already there, the
145−-- blob is unresolved rather than an error, except that a collision making a
141+-- path's blob is unresolved rather than an error.
146−-- path a file on one side and a directory on another, as adding ./a/b onto a
147−-- file ./a would, crashes, naming the path (§7.3).
148replay : Snapshot -> Change -> Snapshot
142replay : Snapshot -> Change -> Snapshot
149unresolved : Blob -> Bool
143unresolved : Blob -> Bool
150−
151−-- Paths edited in the working copy since the last recorded repository state.
152−-- `j status` includes this list; `j save` records it in the focused commit.
153−-- This measures the working copy, even when composed after an in-memory edit.
154−unrecorded : Repo -> [Path]
155touchedPaths : Change -> [Path] -- paths whose content differs, one pass;
144touchedPaths : Change -> [Path] -- paths whose content differs, one pass;
156 -- blobs compared by content id, bytes never read
145 -- blobs compared by content id, bytes never read
157blob : Text -> Blob -- a resolved regular file
146blob : Text -> Blob -- a resolved regular file
158−blobHex : Text -> Text -> Blob -- kind (regular/executable/symlink), hex bytes
159text : Blob -> Text -- its content (markers if unresolved)
147text : Blob -> Text -- its content (markers if unresolved)
160by : Id -> Repo -> Repo -- focus the commit with this id; crash if absent
148by : Id -> Repo -> Repo -- focus the commit with this id; crash if absent
161−lookupCommit : Id -> Repo -> Commit -- indexed commit lookup without moving the focus
162−lookupContent : Path -> Snapshot -> Blob -- indexed file content; an empty blob if absent
163−subtreeCommits : a -> [Commit] -- every commit of a tree in preorder, in one
164− -- pass: what `commits` (below) is
165meta : Id -> Meta -- git hash, author, email, committer time;
149meta : Id -> Meta -- git hash, author, email, committer time;
166 -- crash for a commit not yet persisted
150 -- crash for a commit not yet persisted
167validate : Repo -> Repo -- the repo unchanged, or the crash persisting
151validate : Repo -> Repo -- the repo unchanged, or the crash persisting
168− -- it would produce (shape, root, labels, immutability,
152+ -- it would produce (shape, labels, immutability,
169 -- focus). `tree . validate . rebase trunk` is an
153 -- focus). `tree . validate . rebase trunk` is an
170 -- honest dry run.
154 -- honest dry run.
171diff : Blob -> Blob -> Text -- unified diff from the first to the second
155diff : Blob -> Blob -> Text -- unified diff from the first to the second
172−difft : Path -> Blob -> Blob -> Text -- difftastic when difft is on PATH; otherwise
156+difft : Path -> Blob -> Blob -> Text -- difftastic's rendering; needs difft on PATH.
173− -- unified diff with path headers and 3 context lines,
157+ -- Options via DFT_* environment variables.
174− -- or a binary-files summary. DFT_* configures difft.
175treeWith : { detail : Int, margin : Bool, elide : Bool, icons : Bool, color : Text
158treeWith : { detail : Int, margin : Bool, elide : Bool, icons : Bool, color : Text
176 , lanes : Int, author : Bool, date : Bool, files : Bool }
159 , lanes : Int, author : Bool, date : Bool, files : Bool }
177 -> Repo -> Text -- the history as a tree; see the spec
160 -> Repo -> Text -- the history as a tree; see the spec
@@ -211,11 +194,8 @@ top = \repo -> top (up repo) or repo
211
194
212-- Every commit of a subtree (or of the focused subtree of a Repo), in preorder.
195-- Every commit of a subtree (or of the focused subtree of a Repo), in preorder.
213-- Takes anything with `root` and `children`, so the signature is left open.
196-- Takes anything with `root` and `children`, so the signature is left open.
214−-- The builtin is \t -> t.root :: (concat (map commits t.children) or [])
215−-- in one pass; written that way it copies the list below every commit, and
216−-- so takes time quadratic in the length of the history.
217commits : a -> [Commit]
197commits : a -> [Commit]
218−commits = subtreeCommits
198+commits = \t -> t.root :: (concat (map commits t.children) or [])
219
199
220
200
221------------------------------------------------------------------------------
201------------------------------------------------------------------------------
@@ -259,9 +239,6 @@ restrict = \m -> filter (\e -> m e.path)
259select : Fileset -> Snapshot -> Snapshot -> Snapshot -- m from a, the rest from b
239select : Fileset -> Snapshot -> Snapshot -> Snapshot -- m from a, the rest from b
260select = \m a b -> restrict m a ++ restrict (neg m) b
240select = \m a b -> restrict m a ++ restrict (neg m) b
261
241
262−-- Unresolved paths in a snapshot. For the focus, inspect `j status` or
263−-- `j conflicted . files`, edit the listed files to remove conflict markers,
264−-- then run `j save` and inspect `j status` again. See `j help conflicts`.
265conflicted : Snapshot -> [Path]
242conflicted : Snapshot -> [Path]
266conflicted = \s -> map (.path) (filter (\e -> unresolved e.content) s)
243conflicted = \s -> map (.path) (filter (\e -> unresolved e.content) s)
267
244
@@ -310,7 +287,7 @@ descendants : Revset -- includes the
310descendants = \repo -> map (.id) (commits repo)
287descendants = \repo -> map (.id) (commits repo)
311
288
312ancestors : Revset -- includes the focus
289ancestors : Revset -- includes the focus
313−ancestors = \repo -> repo.root.id :: map (\f -> f.parent.id) repo.context
290+ancestors = \repo -> repo.root.id :: ((let p = up repo in ancestors p) or [])
314
291
315siblings : Revset
292siblings : Revset
316siblings = \repo -> filter (\i -> i /= repo.root.id) (kids (up repo)) or []
293siblings = \repo -> filter (\i -> i /= repo.root.id) (kids (up repo)) or []
@@ -346,11 +323,6 @@ minus = \a b repo -> let ys = b repo in filter (\i -> not (member i ys)) (a repo
346matching : (Commit -> Bool) -> Revset -> Revset
323matching : (Commit -> Bool) -> Revset -> Revset
347matching = \p rs repo -> filter (\i -> p (commitAt i repo)) (rs repo)
324matching = \p rs repo -> filter (\i -> p (commitAt i repo)) (rs repo)
348
325
349−-- Find every commit with unresolved files. `j conflicts` lists candidates;
350−-- use `j by @ID` to focus a mutable one, then `j status` to find its paths.
351−-- Edit each conflicted file, remove the conflict markers, and run `j save`.
352−-- Check `j status` again; `j clean` means the focus has no unresolved files,
353−-- not that the working copy has no unrecorded edits. `j review` shows the diff.
354conflicts : Revset
326conflicts : Revset
355conflicts = matching (\c -> not (null (conflicted c.files))) all
327conflicts = matching (\c -> not (null (conflicted c.files))) all
356
328
@@ -359,10 +331,7 @@ firstOf : [Revset] -> Revset
359firstOf = \rss repo -> foldl (\acc rs -> if null acc then rs repo else acc) [] rss
331firstOf = \rss repo -> foldl (\acc rs -> if null acc then rs repo else acc) [] rss
360
332
361-- The main line: whichever of these names the remote has, first one wins.
333-- The main line: whichever of these names the remote has, first one wins.
362−-- Empty when no fetched remote label is named main, master, or trunk;
334+-- Empty in a repository with no remote, in which case nothing is immutable.
363−-- nothing is then immutable. Configure `j remote URL`, run `j fetch`, and
364−-- inspect `j tree` to see remote labels. A remote with another default label
365−-- needs this definition adjusted. Start work with j 'new . goto trunk'.
366trunk : Revset
335trunk : Revset
367trunk = firstOf [%main %master %trunk]
336trunk = firstOf [%main %master %trunk]
368
337
@@ -378,33 +347,21 @@ immutable = ancestorsOf trunk
378-- Focusing. `goto` is the only place a revset must have exactly one member.
347-- Focusing. `goto` is the only place a revset must have exactly one member.
379------------------------------------------------------------------------------
348------------------------------------------------------------------------------
380
349
381−-- Move to the single commit selected by a revset.
382−-- Example: j 'goto %feature'; use j 'new . goto trunk' to start on the main line.
383goto : Revset -> Edit
350goto : Revset -> Edit
384goto = \rs repo ->
351goto = \rs repo ->
385 let is = rs repo
352 let is = rs repo
386− candidate = \i ->
387− let c = commitAt i repo
388− message = head (splitOn "\n" c.message)
389− in " " ++ show i ++ " " ++ (if message == "" then "(no description)" else message) ++ "\n"
390 in if length is == 1
353 in if length is == 1
391 then by (head is) repo
354 then by (head is) repo
392− else if null is
355+ else crash ("expected one revision, got " ++ show (length is))
393− then crash "expected one revision, got 0; no matching revision. Run `j tree` to inspect available revisions. For a missing remote label such as %main or trunk, configure `j remote URL`, run `j fetch`, and check the label names in `j tree`."
394− else crash ("expected one revision, got " ++ show (length is) ++ "\nCandidates:\n" ++ concat (map candidate (take 8 is)) ++ (if length is > 8 then " More candidates: run `j tree`.\n" else "") ++ "Choose a mutable commit with `j by @ID`, or start a child with `j 'new . by @ID'`; replace @ID with a candidate above.")
395
356
396commitAt : Id -> Repo -> Commit
357commitAt : Id -> Repo -> Commit
397−commitAt = lookupCommit
358+commitAt = \i repo -> (by i repo).root
398
359
399prev : Edit -- the parent
360prev : Edit -- the parent
400prev = goto parents
361prev = goto parents
401
362
402−-- Move to the only child. At a tip, `j new` starts a child; when there are
363+next : Edit -- the only child
403−-- several children, choose one of the candidates with `j by @ID`.
404−next : Edit
405−next = \repo -> if null (kids repo)
406− then crash "next: this commit has no children; run `j new` to start a new child, or `j tree` to inspect the history"
407− else goto kids repo
364+next = goto kids
408
365
409tip : Edit -- end of the line of only-children
366tip : Edit -- end of the line of only-children
410tip = \repo -> tip (next repo) or repo
367tip = \repo -> tip (next repo) or repo
@@ -430,8 +387,6 @@ eachChild = forEach kids
430guard : (Repo -> Bool) -> Edit -- crash unless p holds
387guard : (Repo -> Bool) -> Edit -- crash unless p holds
431guard = \p repo -> if p repo then repo else crash "guard"
388guard = \p repo -> if p repo then repo else crash "guard"
432
389
433−-- Whether the focused commit has no unresolved files. This says nothing
434−-- about unrecorded working-copy edits; inspect those with `j unrecorded`.
435clean : Repo -> Bool
390clean : Repo -> Bool
436clean = \repo -> null (conflicted repo.root.files)
391clean = \repo -> null (conflicted repo.root.files)
437
392
@@ -470,28 +425,11 @@ rewrite = \f repo ->
470-- Commands
425-- Commands
471------------------------------------------------------------------------------
426------------------------------------------------------------------------------
472
427
473−-- Record the current working-copy edits in the focused commit.
474−-- Example: j save; use j status first to inspect unrecorded paths.
475−-- This is id restricted to a Repo: the final Repo result triggers recording.
476−-- It does not publish anything. `j new` starts a child; `j id` records too.
477−save : Edit
478−save = id
479−
480−-- Preview an edit and validate that its result could be recorded, then show
481−-- the resulting tree. This returns Text, so it records nothing.
482−-- Examples: j 'preview squash'; j 'preview (rebase trunk)'.
483−-- This helper is in the reference config. Existing custom configs are not
484−-- upgraded automatically; add it there or use `tree . validate . EDIT`.
485−preview : Edit -> Repo -> Text
486−preview = \edit -> tree . validate . edit
487−
488-- Set the message of the focused commit.
428-- Set the message of the focused commit.
489−-- Example: j 'describe "fix login"'
490describe : Text -> Edit
429describe : Text -> Edit
491describe = \m -> mapRoot (\c -> c { message = m })
430describe = \m -> mapRoot (\c -> c { message = m })
492
431
493-- Start an empty child of the focus and move to it.
432-- Start an empty child of the focus and move to it.
494−-- Examples: j new; j 'describe "next change" . new'
495new : Edit
433new : Edit
496new = \repo -> addChild (newCommit repo.root.files) repo
434new = \repo -> addChild (newCommit repo.root.files) repo
497
435
@@ -499,7 +437,6 @@ new = \repo -> addChild (newCommit repo.root.files) repo
499-- new empty commit on that parent becomes the focus (the parent may be
437-- new empty commit on that parent becomes the focus (the parent may be
500-- immutable, and the focus never is). Refused for a commit the remote has a
438-- immutable, and the focus never is). Refused for a commit the remote has a
501-- name for: push something else under that name first, or forget the name.
439-- name for: push something else under that name first, or forget the name.
502−-- Example: j abandon; validate a preview first with j 'preview abandon'.
503abandon : Edit
440abandon : Edit
504abandon = \repo ->
441abandon = \repo ->
505 let c = repo.root
442 let c = repo.root
@@ -509,20 +446,17 @@ abandon = \repo ->
509 else new (remove (eachChild (replayOnto p) repo))
446 else new (remove (eachChild (replayOnto p) repo))
510
447
511-- Push the files matching m from the focus down into its parent.
448-- Push the files matching m from the focus down into its parent.
512−-- Example: j 'contract (ext "rs")'
513contract : Fileset -> Edit
449contract : Fileset -> Edit
514contract = \m repo -> let a = repo.root
450contract = \m repo -> let a = repo.root
515 in at parents (rewrite (\q -> q { files = select m a.files q.files })) repo
451 in at parents (rewrite (\q -> q { files = select m a.files q.files })) repo
516
452
517-- Fold the focus entirely into its parent; a new empty commit becomes the focus.
453-- Fold the focus entirely into its parent; a new empty commit becomes the focus.
518−-- Example: j squash; preview first with j 'preview squash'.
519squash : Edit
454squash : Edit
520squash = abandon . contract everything
455squash = abandon . contract everything
521
456
522-- Push the files matching m from the focus up into a new child, which becomes
457-- Push the files matching m from the focus up into a new child, which becomes
523-- the focus and inherits the old focus's children. Labels stay with the
458-- the focus and inherits the old focus's children. Labels stay with the
524-- original commit, since they are the remote's.
459-- original commit, since they are the remote's.
525−-- Example: j 'split (ext "rs")'; use (under ./src) to select a directory.
526split : Fileset -> Edit
460split : Fileset -> Edit
527split = \m repo ->
461split = \m repo ->
528 let a = repo.root
462 let a = repo.root
@@ -532,7 +466,6 @@ split = \m repo ->
532 in by x.id (repo { root = a', children = [{ root = x, children = repo.children }] })
466 in by x.id (repo { root = a', children = [{ root = x, children = repo.children }] })
533
467
534-- Move the focused subtree onto another commit. Returns to the moved commit.
468-- Move the focused subtree onto another commit. Returns to the moved commit.
535−-- Example: j 'rebase trunk'; preview with j 'preview (rebase trunk)'.
536rebase : Revset -> Edit
469rebase : Revset -> Edit
537rebase = \rs repo ->
470rebase = \rs repo ->
538 let d = (goto rs repo).root
471 let d = (goto rs repo).root
@@ -543,7 +476,6 @@ rebase = \rs repo ->
543 in by me (attach x.subtree (by d.id x.rest))
476 in by me (attach x.subtree (by d.id x.rest))
544
477
545-- Apply the focused commit's change onto another commit, as a new child there.
478-- Apply the focused commit's change onto another commit, as a new child there.
546−-- Example: j 'pick %main'
547pick : Revset -> Edit
479pick : Revset -> Edit
548pick = \rs repo ->
480pick = \rs repo ->
549 let ch = changeOf repo
481 let ch = changeOf repo
@@ -551,7 +483,6 @@ pick = \rs repo ->
551 in at rs (addChild (newCommit (replay t.files ch))) repo
483 in at rs (addChild (newCommit (replay t.files ch))) repo
552
484
553-- A new child of the focus that undoes the change of the given commit.
485-- A new child of the focus that undoes the change of the given commit.
554−-- Example: j 'backout %feature'
555backout : Revset -> Edit
486backout : Revset -> Edit
556backout = \rs repo ->
487backout = \rs repo ->
557 let ch = changeOf (goto rs repo)
488 let ch = changeOf (goto rs repo)
@@ -597,53 +528,38 @@ entryAt = \p s -> filter (\e -> e.path == p) s
597
528
598-- The content at path p, or an empty file if absent.
529-- The content at path p, or an empty file if absent.
599contentAt : Path -> Snapshot -> Blob
530contentAt : Path -> Snapshot -> Blob
600−contentAt = lookupContent
531+contentAt = \p s -> (head (entryAt p s)).content or blob ""
601
532
602-- The paths a change touches.
533-- The paths a change touches.
603touched : Change -> [Path]
534touched : Change -> [Path]
604touched = touchedPaths
535touched = touchedPaths
605
536
606−-- Paths changed by the focused commit compared with its parent, including
537+-- Paths whose content differs between the focus and its parent.
607−-- the working-copy snapshot. These can remain nonempty after `j save`;
608−-- `j unrecorded` shows only working-copy edits not yet recorded.
609−-- Example: j changed
610changed : Repo -> [Path]
538changed : Repo -> [Path]
611changed = \repo -> touched (changeOf repo)
539changed = \repo -> touched (changeOf repo)
612
540
613−-- What the focus changes, one record per changed path. Uses difftastic when
541+-- What the focus changes, rendered by difftastic, one record per changed path.
614−-- installed; a missing difft falls back to a unified diff with path headers
542+-- Replace difft with (\p -> diff) to use the built-in unified diff instead.
615−-- and 3 context lines, or a binary-files summary.
616diffs : Repo -> [{ path : Path, diff : Text }]
543diffs : Repo -> [{ path : Path, diff : Text }]
617diffs = \repo ->
544diffs = \repo ->
618 let ch = changeOf repo
545 let ch = changeOf repo
619 in map (\p -> { path = p, diff = difft p (contentAt p ch.from) (contentAt p ch.to) }) (touched ch)
546 in map (\p -> { path = p, diff = difft p (contentAt p ch.from) (contentAt p ch.to) }) (touched ch)
620
547
621-- The same, as one page of text. A Text result prints raw, so `j review` is
548-- The same, as one page of text. A Text result prints raw, so `j review` is
622−-- readable in the terminal. A focus that changes nothing reads "no changes";
549+-- readable in the terminal.
623−-- missing difft uses the built-in unified diff (path headers, 3 context lines)
624−-- or a binary-files summary. Other execution errors remain visible; an
625−-- installed difft's output, even nothing, is the page.
626−-- Example: j review; difftastic is optional.
627review : Repo -> Text
550review : Repo -> Text
628−review = \repo ->
551+review = \repo -> concat (map (\d -> d.diff) (diffs repo)) or "no changes\n"
629− let ds = diffs repo
552+
630− in if null ds then "no changes\n" else concat (map (\d -> d.diff) ds)
631−
632−-- A summary of the focus. `changed` lists changes in the commit compared
553+-- A summary of the focus.
633−-- with its parent; `unrecorded` lists working-copy edits not yet recorded.
634−-- Inspection does not record anything. Run `j save` to record those edits.
635−-- Example: j status
636−status : Repo -> { id : Id, message : Text, labels : [Label], changed : [Path], conflicts : [Path], unrecorded : [Path] }
554+status : Repo -> { id : Id, message : Text, labels : [Label], changed : [Path], conflicts : [Path] }
637status = \repo ->
555status = \repo ->
638 { id = repo.root.id
556 { id = repo.root.id
639 , message = repo.root.message
557 , message = repo.root.message
640 , labels = repo.root.labels
558 , labels = repo.root.labels
641 , changed = changed repo
559 , changed = changed repo
642− , conflicts = conflicted repo.root.files
560+ , conflicts = conflicted repo.root.files }
643− , unrecorded = unrecorded repo }
644
561
645-- A summary of every commit, top first, in preorder.
562-- A summary of every commit, top first, in preorder.
646−-- Example: j log
647log : Repo -> [{ id : Id, message : Text, labels : [Label], focus : Bool }]
563log : Repo -> [{ id : Id, message : Text, labels : [Label], focus : Bool }]
648log = \repo ->
564log = \repo ->
649 map (\c -> { id = c.id
565 map (\c -> { id = c.id
@@ -656,7 +572,6 @@ log = \repo ->
656-- (The parentheses are required: `f { … }` would be a record update of f.)
572-- (The parentheses are required: `f { … }` would be a record update of f.)
657-- The default `tree` shows every commit, sorted by time (parents always above
573-- The default `tree` shows every commit, sorted by time (parents always above
658-- their children); `treeCompact` folds uninteresting runs and far subtrees.
574-- their children); `treeCompact` folds uninteresting runs and far subtrees.
659−-- Examples: j tree; j 'tree . squash' previews an edit without saving it.
660tree : Repo -> Text
575tree : Repo -> Text
661tree = treeWith ({ detail = 1, margin = false, elide = false, icons = false, color = "auto", lanes = 4, author = false, date = false, files = false })
576tree = treeWith ({ detail = 1, margin = false, elide = false, icons = false, color = "auto", lanes = 4, author = false, date = false, files = false })
662
577
docs/base.md+46−187
@@ -47,13 +47,6 @@ user = { name = "Your Name", email = "you@example.com" }
47
47
48This is a plain value, not a function. Its signature is checked once, at load
48This is a plain value, not a function. Its signature is checked once, at load
49(§4.13): if you write `user = 5`, `j` refuses to start (exit 3).
49(§4.13): if you write `user = 5`, `j` refuses to start (exit 3).
50−Creating a repository or saving changes also rejects empty fields and the
51−reference placeholders. Use your own name and email before `j init`,
52−`j clone`, or an edit that saves changes. `j help config` shows your actual
53−config path and an example, even if the config is broken.
54−For a new installation, `j setup 'Your actual name' you@your-domain.example`
55−creates the complete config with that identity. It never overwrites an
56−existing file.
57
50
58### 1.2 Paths, labels, entries, snapshots
51### 1.2 Paths, labels, entries, snapshots
59
52
@@ -185,34 +178,6 @@ id 42 => 42
185edit or id -- run edit, keep the repository if it crashes
178edit or id -- run edit, keep the repository if it crashes
186```
179```
187
180
188−### `save` — record file edits
189−
190−```
191−save : Edit
192−```
193−
194−The identity edit, with a name for the everyday action: `j save` records the
195−working-directory snapshot without changing the commit's description or
196−focus. It creates no operation when nothing changed. `j id` does the same.
197−
198−### `preview` — validate an edit without saving
199−
200−```
201−preview : Edit -> Repo -> Text
202−```
203−
204−Apply the edit, validate its result, and render the resulting tree. Because
205−the final value is text, no edit or working-directory snapshot is recorded.
206−Validation errors report why the edit could not be saved.
207−
208−```
209−j 'preview squash'
210−j 'preview (rebase trunk)'
211−```
212−
213−This is the convenient form of `tree . validate . EDIT`. `validate . EDIT`
214−alone returns a repository and saves it.
215−
216### `const` — constant function
181### `const` — constant function
217
182
218```
183```
@@ -291,14 +256,12 @@ not : Bool -> Bool
291```
256```
292
257
293`&&` and `||` short-circuit (the right operand is not evaluated if the left
258`&&` and `||` short-circuit (the right operand is not evaluated if the left
294−decides the result). Operands must be `Bool`. Infix use requires their
259+decides the result). Operands must be `Bool`.
295−declarations in `config.j` and checks the contracts of evaluated operands
296−and the result; a skipped right operand is not checked.
297
260
298```
261```
2991 == 1 && 2 == 2 => true
2621 == 1 && 2 == 2 => true
3001 == 2 || 3 == 3 => true
2631 == 2 || 3 == 3 => true
301−not (1 == 2) => true
264+not (1 == 2) => false
302not true => false
265not true => false
303```
266```
304
267
@@ -338,24 +301,18 @@ Defined on `Int` only; crash on anything else. Non-associative (cannot chain:
338show : a -> Text
301show : a -> Text
339```
302```
340
303
341−When it succeeds, `show` renders a value in the language's literal syntax.
304+Renders any value in the language's literal syntax, such that (except for
342−Except for closures and unresolved blobs, the text parses and evaluates back
305+closures and unresolved blobs) the text parses and evaluates back to an equal
343−to an equal value. A top-level `Text` prints raw, so `j show …` is how you inspect values.
306+value. A top-level `Text` prints raw, so `j show …` is how you inspect values.
344See §5.2 of the spec for the exact rendering.
307See §5.2 of the spec for the exact rendering.
345
308
346−Regular UTF-8 blobs render as `blob "…"`; binary blobs, executables, and
347−symlinks render as `blobHex "kind" "…"`, preserving bytes and file type with
348−lowercase hexadecimal content. An older custom config without
349−`blobHex : Text -> Text -> Blob` must add that declaration before showing
350−these values; otherwise `show` crashes with instructions to do so.
351−
352```
309```
353show 42 => "42"
310show 42 => "42"
354show "hi" => "\"hi\""
311show "hi" => "\"hi\""
355show [1 "a" true] => "[1 \"a\" true]"
312show [1 "a" true] => "[1 \"a\" true]"
356show ({ x = 1 }) => "{ x = 1 }"
313show ({ x = 1 }) => "{ x = 1 }"
357show (describe "wip") => "describe \"wip\""
314show (describe "wip") => "describe \"wip\""
358−show (\x -> x + 1) => "\\x -> x + 1"
315+show (\x -> x + 1) => "\x -> (x + 1)"
359```
316```
360
317
361---
318---
@@ -458,14 +415,8 @@ replay : Snapshot -> Change -> Snapshot
458```
415```
459
416
460`replay onto ch` computes "`onto`, with the change from `ch.from` to `ch.to`
417`replay onto ch` computes "`onto`, with the change from `ch.from` to `ch.to`
461−applied." Where the change collides with what is already there, the path's
418+applied." It is **total**: where the change collides with what is already
462−blob becomes *unresolved* (a conflict) rather than an error. The one
419+there, the path's blob becomes *unresolved* (a conflict) rather than an error.
463−exception is a collision that would make a path a file on one side and a
464−directory on another, as adding `./a/b` onto a file `./a` would: one entry
465−cannot list a directory's entries, so `replay` crashes, naming the path, and
466−`or` catches it. A conflict already at the path in `onto`, `ch.from` or
467−`ch.to` is carried, directory sides and all, so a child that resolves such a
468−conflict can still be squashed into it.
469
420
470```
421```
471-- onto has "base", change takes "base" -> "theirs": no collision
422-- onto has "base", change takes "base" -> "theirs": no collision
@@ -484,12 +435,11 @@ unresolved ((head (replay [({ path = ./f, content = blob "onto" })]
484Two laws to internalise it: `replay b { from = b, to = x } = x` and
435Two laws to internalise it: `replay b { from = b, to = x } = x` and
485`replay x { from = b, to = b } = x`.
436`replay x { from = b, to = b } = x`.
486
437
487−### `unresolved`, `blob`, `blobHex`, `text` — blob primitives
438+### `unresolved`, `blob`, `text` — blob primitives
488
439
489```
440```
490unresolved : Blob -> Bool
441unresolved : Blob -> Bool
491blob : Text -> Blob
442blob : Text -> Blob
492−blobHex : Text -> Text -> Blob
493text : Blob -> Text
443text : Blob -> Text
494```
444```
495
445
@@ -497,16 +447,9 @@ text : Blob -> Text
497blob's content (its conflict-marker rendering if unresolved). `unresolved b`
447blob's content (its conflict-marker rendering if unresolved). `unresolved b`
498is `true` iff the blob is a conflict.
448is `true` iff the blob is a conflict.
499
449
500−`blobHex kind hex` constructs a resolved blob from its file kind and bytes.
501−The kind must be `"regular"`, `"executable"`, or `"symlink"`; the bytes must
502−be an even-length hexadecimal text, using uppercase or lowercase letters.
503−An unknown kind or malformed hex crashes. `text` crashes on non-UTF-8 bytes;
504−persisting a symlink also requires a UTF-8 target.
505−
506```
450```
507blob "content" => blob "content"
451blob "content" => blob "content"
508text (blob "content") => "content"
452text (blob "content") => "content"
509−text (blobHex "regular" "6869") => "hi"
510unresolved (blob "x") => false
453unresolved (blob "x") => false
511```
454```
512
455
@@ -524,24 +467,6 @@ no such visible commit. Backed by the change-id index. Applied to one argument
524by @wqzt -- an edit: refocus on the commit @wqzt
467by @wqzt -- an edit: refocus on the commit @wqzt
525```
468```
526
469
527−### `subtreeCommits` — every commit of a tree
528−
529−```
530−subtreeCommits : a -> [Commit]
531−```
532−
533−Every commit of a tree (anything with `root` and `children`, so a `Subtree`
534−or a `Repo`'s focused subtree), in preorder, in one pass. It gives exactly
535−what `\t -> t.root :: (concat (map subtreeCommits t.children) or [])` gives,
536−but in time linear in the size of the tree: written that way, every level
537−copies the list below it. The reference config's `commits` is this builtin
538−(§6).
539−
540−```
541−subtreeCommits ({ root = 1, children = [({ root = 2, children = [] })] })
542− => [1 2]
543−```
544−
545### `meta` — commit metadata
470### `meta` — commit metadata
546
471
547```
472```
@@ -557,20 +482,15 @@ meta @wqzt => { hash = "74bee64e…", author = "Test User",
557 email = "test@example.com", time = 1789457265 }
482 email = "test@example.com", time = 1789457265 }
558```
483```
559
484
560−### `validate` — check whether persistence would succeed
485+### `validate` — dry-run persistence
561
486
562```
487```
563validate : Repo -> Repo
488validate : Repo -> Repo
564```
489```
565
490
566Returns the repo unchanged if persisting it would succeed; otherwise crashes
491Returns the repo unchanged if persisting it would succeed; otherwise crashes
567−with exactly the error persistence would produce (shape, root, labels, immutability,
492+with exactly the error persistence would produce (shape, labels, immutability,
568−focus). A changed repository also checks that the author identity is configured;
569−an unchanged repository can still be inspected with the default identity.
570−`tree . validate . rebase trunk` is an honest dry run.
493+focus). `tree . validate . rebase trunk` is an honest dry run.
571−`validate . rebase trunk` alone saves the result: `validate` returns the same
572−repository it was given, not the repository from before the edit. Prefer
573−`preview (rebase trunk)` for the complete safe-preview expression.
574
494
575### `diff` and `difft` — rendering differences
495### `diff` and `difft` — rendering differences
576
496
@@ -588,34 +508,25 @@ diff (blob "a\nb\n") (blob "a\nc\n")
588```
508```
589
509
590`difft p a b` renders the comparison with **difftastic**, treating the input
510`difft p a b` renders the comparison with **difftastic**, treating the input
591−as the file `p` (its last component guides language detection), using options
511+as the file `p` (its last component guides language detection). Needs `difft`
592−from `DFT_*` environment variables. The Nix package provides `difft`.
512+on `PATH` (the Nix package provides it); options via `DFT_*` environment
593−When the executable is missing, the builtin returns a unified diff with
513+variables. Crashes naming the tool if absent.
594−`a/` and `b/` path headers and three lines of context; binary content receives
595−a difference summary. Other launch failures are crashes naming the tool and
596−OS error. A `difft` that runs and fails is not an error: what it printed,
597−even nothing, is the result, and what it says on stderr reaches the terminal.
598
514
599### `treeWith` — the history tree
515### `treeWith` — the history tree
600
516
601```
517```
602−treeWith : { detail : Int, margin : Bool, elide : Bool, icons : Bool, color : Text,
518+treeWith : { detail : Int, margin : Bool, elide : Bool, icons : Bool, color : Text }
603− lanes : Int, author : Bool, date : Bool, files : Bool }
604 -> Repo -> Text
519 -> Repo -> Text
605```
520```
606
521
607Renders the history as a tree with an options record. The reference config
522Renders the history as a tree with an options record. The reference config
608−provides ready-made settings, including `tree` and `treeFull` (§10).
523+provides two ready-made settings, `tree` and `treeFull` (§9).
609
524
610```
525```
611−treeWith ({ detail = 2, margin = true, elide = false, icons = false, color = "auto", lanes = 4, author = false, date = false, files = false })
526+treeWith ({ detail = 2, margin = true, elide = false, icons = false, color = "auto" })
612 -- a full tree with ages and authors in the margin
527 -- a full tree with ages and authors in the margin
613```
528```
614
529
615−Branches beyond the `lanes` limit retain their state marker beside `»`.
616−Increase `lanes` to display more rails. Long file paths in detail rows wrap
617−to the available terminal width.
618−
619### `extract` — every subvalue of a shape
530### `extract` — every subvalue of a shape
620
531
621```
532```
@@ -677,9 +588,7 @@ top = \repo -> top (up repo) or repo
677```
588```
678
589
679Moves the focus all the way to the top of history (where `context = []`). The
590Moves the focus all the way to the top of history (where `context = []`). The
680−`or repo` is the base case: when `up` crashes at the top, stop. Use it inside
591+`or repo` is the base case: when `up` crashes at the top, stop.
681−an inspection such as `commits . top` or `tree . top`; `j top` alone cannot
682−save a focus on the protected root commit.
683
592
684```
593```
685top -- refocus at the very first commit
594top -- refocus at the very first commit
@@ -690,16 +599,13 @@ commits . top -- every commit, preorder
690
599
691```
600```
692commits : a -> [Commit]
601commits : a -> [Commit]
693−commits = subtreeCommits
602+commits = \t -> t.root :: (concat (map commits t.children) or [])
694```
603```
695
604
696Every commit of a subtree (or of the focused subtree of a `Repo`), in
605Every commit of a subtree (or of the focused subtree of a `Repo`), in
697preorder. Takes anything with `root` and `children`, so the signature is left
606preorder. Takes anything with `root` and `children`, so the signature is left
698−open. It is the builtin `subtreeCommits` (§5), which computes
699−`\t -> t.root :: (concat (map commits t.children) or [])` in one pass. (There
700−the `or []` handles a leaf, whose `children` is empty, so `concat` crashes and
607+open. The `or []` handles a leaf (whose `children` is empty, so `concat`
701−yields `[]`.) Written in the language, it copies the list below every commit,
608+crashes and yields `[]`).
702−and takes time quadratic in the length of the history.
703
609
704```
610```
705length . commits . top -- how many commits are visible?
611length . commits . top -- how many commits are visible?
@@ -860,28 +766,26 @@ is `new . goto trunk`, never `goto trunk` alone.
860
766
861**`goto : Revset -> Edit`** — refocus on *the one commit* a revset names. This
767**`goto : Revset -> Edit`** — refocus on *the one commit* a revset names. This
862is the only place a revset must have exactly one member; otherwise it crashes
768is the only place a revset must have exactly one member; otherwise it crashes
863−with `expected one revision, got N` and recovery guidance. An empty selector
769+with `expected one revision, got N`.
864−points to `j tree` and remote/fetch setup; an ambiguous selector shows up to
770+
865−eight candidate ids and descriptions, with commands to select a mutable commit
771+```
866−or create a child. Candidate ids are complete copyable id literals.
772+goto = \rs repo ->
773+ let is = rs repo
774+ in if length is == 1 then by (head is) repo
775+ else crash ("expected one revision, got " ++ show (length is))
776+```
867
777
868```
778```
869−new . goto %main -- start a mutable child on main
779+goto %main -- refocus on main (crash if it names 0 or 2+ commits)
870−tree . goto %main -- inspect main without moving the working copy
871goto parents -- == prev
780goto parents -- == prev
872```
781```
873
782
874**`commitAt : Id -> Repo -> Commit`** — the commit with a given id.
783**`commitAt : Id -> Repo -> Commit`** — the commit with a given id.
875−The reference definition uses `lookupCommit`, which indexes the repository
876−once and retrieves commits without moving the focus. Its result is the same
877−as `(by i repo).root`.
878
784
879−**`prev : Edit`** — to the parent (`goto parents`). Like every navigation edit,
785+**`prev : Edit`** — to the parent (`goto parents`).
880−it checks out the selected commit. A protected parent cannot remain focused;
881−inspect it with `tree . prev` or start a new child instead.
882
786
883−**`next : Edit`** — to the only child. At a tip it suggests `j new`; at a fork
787+**`next : Edit`** — to the only child (`goto kids`; crashes if the focus has
884−it lists candidates to choose with `by`.
788+other than exactly one child).
885
789
886**`tip : Edit`** — the end of the line of only-children (`tip = \repo -> tip
790**`tip : Edit`** — the end of the line of only-children (`tip = \repo -> tip
887(next repo) or repo`).
791(next repo) or repo`).
@@ -1065,9 +969,6 @@ Crashes at the top.
1065
969
1066`j push EXPR` evaluates `EXPR` to a list of `Push` and `Drop` records. These
970`j push EXPR` evaluates `EXPR` to a list of `Push` and `Drop` records. These
1067functions build such lists.
971functions build such lists.
1068−Push sends recorded commits. Unrecorded file edits produce a warning before
1069−the recorded content is sent. Run `j id`, then push again to include those
1070−edits. Inspection commands such as `j status` do not record them.
1071
972
1072**`label : Label -> Revset -> Repo -> [Push]`** — label every commit of a
973**`label : Label -> Revset -> Repo -> [Push]`** — label every commit of a
1073revset.
974revset.
@@ -1100,8 +1001,7 @@ j 'push relabel' -- after rewriting, re-point the remote's bookmarks
1100
1001
1101## 10. Inspection
1002## 10. Inspection
1102
1003
1103−These return printable values rather than a `Repo`. They include current
1004+These return printable values rather than a `Repo`.
1104−file edits in the displayed snapshot without recording those edits.
1105
1005
1106**`focus : Repo -> Commit`** — the focused commit.
1006**`focus : Repo -> Commit`** — the focused commit.
1107
1007
@@ -1116,20 +1016,6 @@ j 'files' -- the working-copy commit's files
1116
1016
1117**`contentAt : Path -> Snapshot -> Blob`** — the content at a path, or an
1017**`contentAt : Path -> Snapshot -> Blob`** — the content at a path, or an
1118empty file if absent.
1018empty file if absent.
1119−The reference definition uses `lookupContent`, which shares a path index
1120−between repeated lookups in each snapshot. Duplicate paths still select the
1121−first entry, as `head (entryAt p s)` does.
1122−
1123−Existing user configurations keep working with their original definitions.
1124−To enable both indexed lookups, add these builtin declarations to your config:
1125−
1126−```
1127−lookupCommit : Id -> Repo -> Commit
1128−lookupContent : Path -> Snapshot -> Blob
1129−```
1130−
1131−Then replace the bodies of `commitAt` and `contentAt` with `lookupCommit` and
1132−`lookupContent`, respectively, keeping their existing signatures.
1133
1019
1134```
1020```
1135contentAt ./a [({ path = ./a, content = blob "x" })]
1021contentAt ./a [({ path = ./a, content = blob "x" })]
@@ -1142,64 +1028,42 @@ contentAt ./missing [] => blob ""
1142**`changed : Repo -> [Path]`** — paths whose content differs between the focus
1028**`changed : Repo -> [Path]`** — paths whose content differs between the focus
1143and its parent.
1029and its parent.
1144
1030
1145−**`unrecorded : Repo -> [Path]`** — paths changed in the working directory
1146−since the recorded snapshot at the start of this invocation. This builtin
1147−compares the loaded and snapshotted focus files, independently of edits to
1148−the repository value later in the expression. It returns `[]` when no
1149−snapshot context exists, including in a push expression.
1150−
1151**`diffs : Repo -> [{ path : Path, diff : Text }]`** — what the focus changes,
1031**`diffs : Repo -> [{ path : Path, diff : Text }]`** — what the focus changes,
1152−rendered by difftastic, one record per changed path, with unified diffs as
1032+rendered by difftastic, one record per changed path. (Replace `difft` with
1153−the fallback when difftastic is missing.
1033+`(\p -> diff)` to use the built-in unified diff instead.)
1154
1034
1155−**`review : Repo -> Text`** — the same, as one page of text, or
1035+**`review : Repo -> Text`** — the same, as one page of text. A `Text` result
1156−`"no changes\n"` when the focus changes nothing. A `Text` result prints raw,
1157−so `j review` is readable in the terminal even without difftastic. When
1036+prints raw, so `j review` is readable in the terminal.
1158−`difft` runs, whatever it prints, even nothing, is the page, and what it says
1159−on stderr reaches the terminal. Other launch errors still fail the review.
1160
1037
1161```
1038```
1162j 'review' -- the focus's changes, as difftastic renders them
1039j 'review' -- the focus's changes, as difftastic renders them
1163```
1040```
1164
1041
1165−**`status : Repo -> { id : Id, message : Text, labels : [Label], changed : [Path], unrecorded : [Path], conflicts : [Path] }`** — a summary of the focus and unsaved file edits.
1042+**`status : Repo -> { id : Id, message : Text, labels : [Label], changed : [Path], conflicts : [Path] }`** — a summary of the focus.
1166
1043
1167```
1044```
1168j 'status'
1045j 'status'
1169-- id @wqzt…
1046-- id @wqzt…
1170-- message add the lexer
1047-- message add the lexer
1171-- labels [feature]
1048-- labels [feature]
1172−-- in commit [src/lexer.rs]
1049+-- changed [src/lexer.rs]
1173−-- conflicts No conflicts
1050+-- conflicts []
1174−-- unrecorded All edits recorded
1175```
1051```
1176
1052
1177−Empty changed paths, labels, and descriptions read `No changes`, `No labels`,
1178−and `No description` respectively. The display calls `changed` **in commit**;
1179−the underlying field keeps its name. It compares the focus with its
1180−parent. `unrecorded` lists paths that still need saving and reads
1181−`All edits recorded` when empty. Run `j save` to record them; `changed`
1182−continues to show the commit's changes after saving.
1183−
1184**`log : Repo -> [{ id : Id, message : Text, labels : [Label], focus : Bool }]`**
1053**`log : Repo -> [{ id : Id, message : Text, labels : [Label], focus : Bool }]`**
1185— a summary of every commit, top first, in preorder (`focus` marks the focused
1054— a summary of every commit, top first, in preorder (`focus` marks the focused
1186−commit). The display shows short ids and label names, fits columns to the
1055+commit).
1187−terminal, and switches to fields on separate lines when necessary. Redirected
1188−output preserves full cell text.
1189
1056
1190**`tree : Repo -> Text`** and **`treeFull : Repo -> Text`** — the history as a
1057**`tree : Repo -> Text`** and **`treeFull : Repo -> Text`** — the history as a
1191−tree, built with `treeWith`. `tree` shows every commit (`detail = 1`);
1058+tree, built with `treeWith`. `tree` is compact (`detail = 1`, elided);
1192`treeFull` shows ages and authors in the margin (`detail = 2`, `margin =
1059`treeFull` shows ages and authors in the margin (`detail = 2`, `margin =
1193true`). Edit these definitions to change what `j tree` shows.
1060true`). Edit these definitions to change what `j tree` shows.
1194
1061
1195```
1062```
1196−tree = treeWith ({ detail = 1, margin = false, elide = false, icons = false, color = "auto", lanes = 4, author = false, date = false, files = false })
1063+tree = treeWith ({ detail = 1, margin = false, elide = true, icons = false, color = "auto" })
1197−treeFull = treeWith ({ detail = 2, margin = true, elide = false, icons = false, color = "auto", lanes = 4, author = false, date = false, files = false })
1064+treeFull = treeWith ({ detail = 2, margin = true, elide = false, icons = false, color = "auto" })
1198```
1065```
1199
1066
1200−Use `treeCompact` to fold uninteresting runs and distant subtrees, or
1201−`treeData` for extra date, files-changed, and author columns.
1202−
1203```
1067```
1204j 'tree'
1068j 'tree'
1205-- ⌂ zzzz
1069-- ⌂ zzzz
@@ -1229,7 +1093,6 @@ j 'tree'
1229| `attach` | `Subtree -> Edit` | §7.1 |
1093| `attach` | `Subtree -> Edit` | §7.1 |
1230| `backout` | `Revset -> Edit` | §8 |
1094| `backout` | `Revset -> Edit` | §8 |
1231| `blob` | `Text -> Blob` | §5 |
1095| `blob` | `Text -> Blob` | §5 |
1232−| `blobHex` | `Text -> Text -> Blob` | §5 |
1233| `both` | `Fileset -> Fileset -> Fileset` | §7.2 |
1096| `both` | `Fileset -> Fileset -> Fileset` | §7.2 |
1234| `by` | `Id -> Repo -> Repo` | §5 |
1097| `by` | `Id -> Repo -> Repo` | §5 |
1235| `changeOf` | `Repo -> Change` | §7.2 |
1098| `changeOf` | `Repo -> Change` | §7.2 |
@@ -1299,7 +1162,6 @@ j 'tree'
1299| `parents` | `Revset` | §7.3 |
1162| `parents` | `Revset` | §7.3 |
1300| `pick` | `Revset -> Edit` | §8 |
1163| `pick` | `Revset -> Edit` | §8 |
1301| `prev` | `Edit` | §7.4 |
1164| `prev` | `Edit` | §7.4 |
1302−| `preview` | `Edit -> Repo -> Text` | §2 |
1303| `range` | `Int -> Int -> [Int]` | §4 |
1165| `range` | `Int -> Int -> [Int]` | §4 |
1304| `rebase` | `Revset -> Edit` | §8 |
1166| `rebase` | `Revset -> Edit` | §8 |
1305| `relabel` | `Repo -> [Push]` | §9 |
1167| `relabel` | `Repo -> [Push]` | §9 |
@@ -1311,7 +1173,6 @@ j 'tree'
1311| `restrict` | `Fileset -> Snapshot -> Snapshot` | §7.2 |
1173| `restrict` | `Fileset -> Snapshot -> Snapshot` | §7.2 |
1312| `review` | `Repo -> Text` | §10 |
1174| `review` | `Repo -> Text` | §10 |
1313| `rewrite` | `(Commit -> Commit) -> Edit` | §8 |
1175| `rewrite` | `(Commit -> Commit) -> Edit` | §8 |
1314−| `save` | `Edit` | §2 |
1315| `select` | `Fileset -> Snapshot -> Snapshot -> Snapshot` | §7.2 |
1176| `select` | `Fileset -> Snapshot -> Snapshot -> Snapshot` | §7.2 |
1316| `show` | `a -> Text` | §3 |
1177| `show` | `a -> Text` | §3 |
1317| `siblings` | `Revset` | §7.3 |
1178| `siblings` | `Revset` | §7.3 |
@@ -1321,7 +1182,6 @@ j 'tree'
1321| `stack` | `Revset` | §7.3 |
1182| `stack` | `Revset` | §7.3 |
1322| `startsWith` | `Text -> Text -> Bool` | §4 |
1183| `startsWith` | `Text -> Text -> Bool` | §4 |
1323| `status` | `Repo -> {…}` | §10 |
1184| `status` | `Repo -> {…}` | §10 |
1324−| `subtreeCommits` | `a -> [Commit]` | §5 |
1325| `tail` | `[a] -> [a]` | §4 |
1185| `tail` | `[a] -> [a]` | §4 |
1326| `take` | `Int -> [a] -> [a]` | §4 |
1186| `take` | `Int -> [a] -> [a]` | §4 |
1327| `text` | `Blob -> Text` | §5 |
1187| `text` | `Blob -> Text` | §5 |
@@ -1335,7 +1195,6 @@ j 'tree'
1335| `under` | `Path -> Fileset` | §7.2 |
1195| `under` | `Path -> Fileset` | §7.2 |
1336| `union` | `Revset -> Revset -> Revset` | §7.3 |
1196| `union` | `Revset -> Revset -> Revset` | §7.3 |
1337| `unlabel` | `Label -> [Drop]` | §9 |
1197| `unlabel` | `Label -> [Drop]` | §9 |
1338−| `unrecorded` | `Repo -> [Path]` | §10 |
1339| `unresolved` | `Blob -> Bool` | §5 |
1198| `unresolved` | `Blob -> Bool` | §5 |
1340| `up` | `Edit` | §6 |
1199| `up` | `Edit` | §6 |
1341| `user` | `{ name : Text, email : Text }` | §1.1 |
1200| `user` | `{ name : Text, email : Text }` | §1.1 |
docs/comparison.md+9−25
@@ -75,27 +75,21 @@ about: a commit keeps its identity (`@wqzt…`) across `describe`, `squash`,
75`j push` takes an *expression* rather than flags: `j 'push (label "feature"
75`j push` takes an *expression* rather than flags: `j 'push (label "feature"
76here)'` labels the focused commit, `j 'push relabel'` re-points the remote's
76here)'` labels the focused commit, `j 'push relabel'` re-points the remote's
77bookmarks after a rewrite.
77bookmarks after a rewrite.
78−Push sends recorded commits and warns before sending if files have
79−unrecorded edits. Run `j id`, then push again to include those edits.
80
78
81### 3.2 Recording work
79### 3.2 Recording work
82
80
83| Task | git | jj | j |
81| Task | git | jj | j |
84|---|---|---|---|
82|---|---|---|---|
85−| Save current state | `git add … && git commit` | automatic (`jj st` shows it) | an edit records the snapshot; `j save` records *only* the snapshot |
83+| Save current state | `git add … && git commit` | automatic (`jj st` shows it) | automatic; `j id` to record *only* the snapshot |
86| Set the message | `git commit -m "…"` | `jj describe -m "…"` or `jj describe` | `j 'describe "…"'` |
84| Set the message | `git commit -m "…"` | `jj describe -m "…"` or `jj describe` | `j 'describe "…"'` |
87| Start the next change | (just keep editing) | `jj new` | `j new` |
85| Start the next change | (just keep editing) | `jj new` | `j new` |
88| Amend the current commit | `git commit --amend` | `jj describe` + edit files in `@` | edit files + `j 'describe "…"'` |
86| Amend the current commit | `git commit --amend` | `jj describe` + edit files in `@` | edit files + `j 'describe "…"'` |
89| See what changed | `git diff` / `git show` | `jj diff` / `jj show` | `j review` (via difftastic) / `j status` |
87| See what changed | `git diff` / `git show` | `jj diff` / `jj show` | `j review` (via difftastic) / `j status` |
90
88
91−In git, "commit" is a deliberate act with a staging step. jj snapshots the
89+In git, "commit" is a deliberate act with a staging step. In jj and `j`, the
92−working copy automatically. In `j`, expressions see current file edits,
90+snapshot is continuous; `describe` sets a message and `new` opens the next
93−but only a changed `Repo` result records them: `describe` saves a message
91+change. `j` has one deliberate-snapshot command, `j id`, for when you want to
94−and `new` opens the next change. `j status` and other inspections save
95−nothing. Use `j save` (an alias for `id`) to record the working directory and
92+record the working directory and nothing else.
96−nothing else. Status displays the `changed` field as `in commit`, comparing
97−the current commit with its parent; `unrecorded` shows file edits made since
98−the last save.
99
93
100### 3.3 Looking around
94### 3.3 Looking around
101
95
@@ -122,7 +116,7 @@ zipper with edits.
122|---|---|---|---|
116|---|---|---|---|
123| To the parent | `git checkout HEAD~1` | `jj prev` *(≈, opens a new empty commit there)* | `j prev` |
117| To the parent | `git checkout HEAD~1` | `jj prev` *(≈, opens a new empty commit there)* | `j prev` |
124| To the child | `git checkout <sha>` (manual) | `jj next` *(≈, opens a new empty commit there)* | `j next` |
118| To the child | `git checkout <sha>` (manual) | `jj next` *(≈, opens a new empty commit there)* | `j next` |
125−| Start work on a named commit | `git checkout <branch>` | `jj new <rev>` | `j 'new . goto %main'` |
119+| To a named commit | `git checkout <branch>` | `jj new <rev>` | `j 'goto %main'` |
126| To a specific change | `git checkout <sha>` | `jj new <change-id>` | `j 'by @wqzt'` |
120| To a specific change | `git checkout <sha>` | `jj new <change-id>` | `j 'by @wqzt'` |
127| Back to your branch | `git checkout main` | `jj new main` | `j 'new . goto trunk'` |
121| Back to your branch | `git checkout main` | `jj new main` | `j 'new . goto trunk'` |
128
122
@@ -207,16 +201,14 @@ the remote's state explicit and impossible to mutate by accident.
207| When do conflicts appear? | During merge/rebase, blocking it | Recorded *in* the commit, non-blocking | Recorded in the commit (unresolved `Blob`s) |
201| When do conflicts appear? | During merge/rebase, blocking it | Recorded *in* the commit, non-blocking | Recorded in the commit (unresolved `Blob`s) |
208| Where do they live? | The working tree + index | In the commit itself | In the commit's `files`, as conflict blobs |
202| Where do they live? | The working tree + index | In the commit itself | In the commit's `files`, as conflict blobs |
209| How do you see them? | `git status` | `jj st`, `jj resolve` | `j conflicts`, `j status` |
203| How do you see them? | `git status` | `jj st`, `jj resolve` | `j conflicts`, `j status` |
210−| How do you resolve? | Edit files, `git add`, continue | Edit files; conflict is gone from `@` | Edit files, then `j save` and check `j status` |
204+| How do you resolve? | Edit files, `git add`, continue | Edit files; conflict is gone from `@` | Edit files, then any persisting expression (`j id`) |
211
205
212git stops the world on a conflict: a merge or rebase halts until you resolve
206git stops the world on a conflict: a merge or rebase halts until you resolve
213and `git add`. jj and `j` treat a conflict as *data* — a commit can simply
207and `git add`. jj and `j` treat a conflict as *data* — a commit can simply
214*contain* conflicted files, you can keep working, and you resolve whenever you
208*contain* conflicted files, you can keep working, and you resolve whenever you
215like. `j conflicts` lists the commits that have conflicts; checking one out
209like. `j conflicts` lists the commits that have conflicts; checking one out
216−writes the markers to disk; editing the files and running `j save` records
210+writes the markers to disk; editing the files and running `j id` records the
217−the resolution. Check `j status` for `No conflicts` and `All edits recorded`.
211+resolution.
218−There is no separate continuation command. `j review` uses a unified diff
219−when difftastic is not installed.
220
212
221---
213---
222
214
@@ -235,12 +227,6 @@ a first-class, marked operation, so `undo`/`redo` are safe and precise. In
235`j`, undoing a `push` restores the local labels (and leaves the remote
227`j`, undoing a `push` restores the local labels (and leaves the remote
236untouched).
228untouched).
237
229
238−`j undo` and `j redo` require no unrecorded working-copy edits. Recording
239−edits with `j id` adds an operation, so the next undo removes that recording
240−before another undo reaches the preceding edit. A new recording also clears
241−pending redos. Interactive confirmations identify the affected operation and
242−restored focus; `j ops` shows the history.
243−
244---
230---
245
231
246## 9. The interface itself
232## 9. The interface itself
@@ -310,8 +296,6 @@ j new
310# …edit…
296# …edit…
311j 'describe "part two"'
297j 'describe "part two"'
312j squash
298j squash
313−j prev # squash leaves an empty child; return to the combined commit
314−j 'describe "complete feature"'
315j 'rebase %main'
299j 'rebase %main'
316j 'push (label "feature" here)'
300j 'push (label "feature" here)'
317```
301```
docs/language.md+25−91
@@ -3,14 +3,7 @@
3`j` is a small, pure, functional language. Every `j` command evaluates one
3`j` is a small, pure, functional language. Every `j` command evaluates one
4expression in this language against the current repository. The language is
4expression in this language against the current repository. The language is
5the whole interface: there are no subcommands beyond a handful of reserved
5the whole interface: there are no subcommands beyond a handful of reserved
6−words, and no general flags apart from help. `--help` and `-h` open help as
6+words, and no flags.
7−the first word or as a separate argument after a command: `j new --help`
8−shows help without executing `new`. Inside a single expression argument or
9−stdin, `--` still starts a language comment, so `j 'new --help'` executes
10−`new`. Use `j help` for a command guide, `j commands` for everyday tasks,
11−and `j commands all` for the complete vocabulary. `j setup NAME EMAIL`
12−creates a new configuration with your identity without replacing an existing
13−file.
14
7
15This document describes the language itself — its values, syntax, semantics,
8This document describes the language itself — its values, syntax, semantics,
16and the builtins — with examples throughout. Every example shows an expression
9and the builtins — with examples throughout. Every example shows an expression
@@ -24,17 +17,16 @@ and what it evaluates to, exactly as `j` prints it.
24
17
25## 1. The big picture
18## 1. The big picture
26
19
27−An expression run of `j` looks like this:
20+A run of `j` looks like this:
28
21
291. Read the expression (from the command line or stdin).
221. Read the expression (from the command line or stdin).
30−2. Snapshot the working directory for evaluation.
23+2. Snapshot the working directory into the working-copy commit.
313. Build a value `repo` describing the repository, focused on the
243. Build a value `repo` describing the repository, focused on the
32 working-copy commit.
25 working-copy commit.
334. Evaluate the expression.
264. Evaluate the expression.
345. If the result is a function, apply it to `repo`.
275. If the result is a function, apply it to `repo`.
356. If the final value is a `Repo` that differs from the one loaded, persist
286. If the final value is a `Repo` that differs from the one loaded, persist
36− the difference. An unchanged `Repo` saves nothing; other values print
29+ the difference; otherwise print the value and change nothing.
37− without saving.
38
30
39So the most common thing you write is an **edit** — a function `Repo -> Repo`
31So the most common thing you write is an **edit** — a function `Repo -> Repo`
40(or a value that becomes one). `describe "wip"` is an edit. `new` is an edit.
32(or a value that becomes one). `describe "wip"` is an edit. `new` is an edit.
@@ -180,20 +172,8 @@ let r = { a = 1, b = 2 } in r { b = 99 }
180> (\r -> r.name) ({ name = "j" }) -- OK
172> (\r -> r.name) ({ name = "j" }) -- OK
181> ```
173> ```
182>
174>
183−> The same applies inside lists: a record literal that follows another
175+> The same applies inside lists: `[{ root = c, children = [] }]` — write the
184−> element is parenthesised. Written side by side, the second record updates
176+> braces as a parenthesised element `[(…)]` if it follows another element.
185−> the first, so `[{ id = 1 } { id = 2 }]` is the one-element list
186−> `[{ id = 2 }]`:
187−>
188−> ```
189−> [({ id = 1 }) ({ id = 2 })] -- OK: two records
190−> [{ root = c, children = [] }] -- OK: the only element
191−> ```
192−>
193−> A line break stops this: a `.name` or `{` that starts a line never attaches
194−> to what came before, so `r` with `.a` on the next (indented) line is
195−> `r (.a)`, not `r.a`. Keep a selector or update on the line where the
196−> expression it applies to ends.
197
177
198### Functions
178### Functions
199
179
@@ -234,8 +214,7 @@ distinct id each time it is evaluated (see §7).
234
214
235A `Blob` is the content of one file, either resolved or unresolved (a
215A `Blob` is the content of one file, either resolved or unresolved (a
236conflict). You rarely write them directly; you get them from a commit's
216conflict). You rarely write them directly; you get them from a commit's
237−`files`. It carries the file kind as well as its bytes. `blob "…"` builds a
238−resolved regular file from text; `text` reads it back:
217+`files`. `blob "…"` builds a resolved one, `text` reads it back:
239
218
240```
219```
241text (blob "file content")
220text (blob "file content")
@@ -286,10 +265,9 @@ in even 10
286> binding column.
265> binding column.
287
266
288Because evaluation is strict (call by value), a non-function binding is
267Because evaluation is strict (call by value), a non-function binding is
289−evaluated immediately, though after the bindings it needs, whatever order
268+evaluated immediately. `let a = b; b = 5 in a` crashes (`b` isn't bound yet
290−they are written in: `let a = b; b = 5 in a` is 5. Bindings that need each
269+when `a` is evaluated); recursion works through *functions*, whose bodies
291−other's values, as in `let a = b; b = a in a`, crash; recursion works through
292−*functions*, whose bodies aren't evaluated until applied.
270+aren't evaluated until applied.
293
271
294### No shadowing
272### No shadowing
295
273
@@ -301,12 +279,6 @@ This is an error, not silent shadowing:
301let x = 1 in let f = \x -> x in f 2 -- parse error: `x` is already bound
279let x = 1 in let f = \x -> x in f 2 -- parse error: `x` is already bound
302```
280```
303
281
304−The one exception is inside `config.j`: a binder there may reuse the name of
305−one of the config's own top-level definitions, as the reference config's
306−`newCommit = \files -> …` does, and within that binder's scope the name means
307−the binder. Builtin names and names bound by an enclosing lambda or `let` are
308−still refused there.
309−
310### Lexical scope
282### Lexical scope
311
283
312Names resolve lexically: innermost `let` bindings, then lambda parameters,
284Names resolve lexically: innermost `let` bindings, then lambda parameters,
@@ -382,7 +354,7 @@ A bare `.name` is the accessor function `\x -> x.name`:
382```
354```
383.name ({ name = "j" })
355.name ({ name = "j" })
384 => "j"
356 => "j"
385−map (.id) [({ id = 1 }) ({ id = 2 })]
357+map (.id) ([{ id = 1 } { id = 2 }])
386 => [1 2]
358 => [1 2]
387```
359```
388
360
@@ -561,9 +533,6 @@ crash "my message"
561
533
562Parse errors, configuration errors, and `Id`-resolution failures happen
534Parse errors, configuration errors, and `Id`-resolution failures happen
563*before* evaluation and are **not** catchable.
535*before* evaluation and are **not** catchable.
564−CLI diagnostics identify the source and line for syntax errors and offer
565−quoting, missing-argument, or spelling hints where available. Configuration
566−errors name the file to repair; `j help config` works even with broken config.
567
536
568---
537---
569
538
@@ -647,13 +616,6 @@ unresolved (blob "x") => false
647`unresolved b` is `true` iff the blob is a conflict (produced by a clashing
616`unresolved b` is `true` iff the blob is a conflict (produced by a clashing
648`replay`). `text` on an unresolved blob yields its conflict-marker rendering.
617`replay`). `text` on an unresolved blob yields its conflict-marker rendering.
649
618
650−`blobHex kind hex` constructs a resolved blob with arbitrary bytes and a
651−file kind: `"regular"`, `"executable"`, or `"symlink"`. Its second argument
652−is an even-length hexadecimal text, accepting either letter case. Unknown
653−kinds and invalid hex crash. For example,
654−`text (blobHex "regular" "6869")` is `"hi"`. `text` crashes on non-UTF-8
655−content; persisting a symlink requires its target to be valid UTF-8 too.
656−
657### Shapes and `extract`
619### Shapes and `extract`
658
620
659A type name in an expression is a `Shape`. `extract S v` returns every
621A type name in an expression is a `Shape`. `extract S v` returns every
@@ -685,10 +647,6 @@ A function that has a **signature** (a builtin, or a definition with one in
685parameter type as it is supplied, and the final result against the result
647parameter type as it is supplied, and the final result against the result
686type.
648type.
687
649
688−Infix `&&` and `||` require their declarations in `config.j` and check the
689−contracts of each evaluated operand and the result. Short-circuiting skips
690−both evaluation and checking of the right operand.
691−
692The check is one level deep — it never inspects inside a list or a record's
650The check is one level deep — it never inspects inside a list or a record's
693field values (that would make every call cost the size of the value).
651field values (that would make every call cost the size of the value).
694
652
@@ -718,17 +676,11 @@ unchecked except through the functions they call.
718
676
719## 12. `show`: rendering values as text
677## 12. `show`: rendering values as text
720
678
721−When it succeeds, `show v` renders a value as an expression in the language.
679+`show v` renders any value as an expression in the language, such that
722−Except for closures and unresolved blobs, the result parses and evaluates
680+(except for closures and unresolved blobs) the result parses and evaluates
723back to an equal value. Since `show` returns a `Text`, and a top-level `Text`
681back to an equal value. Since `show` returns a `Text`, and a top-level `Text`
724prints raw, `j show …` is how you inspect values.
682prints raw, `j show …` is how you inspect values.
725
683
726−Regular blobs with UTF-8 content render as `blob "…"`. Other resolved blobs
727−render as `blobHex "kind" "…"` with lowercase hexadecimal bytes, preserving
728−binary content and executable or symlink kinds. If an older custom config
729−lacks `blobHex : Text -> Text -> Blob`, `show` crashes with instructions to
730−add it when such a value is encountered. Existing configs are not modified.
731−
732```
684```
733show 42 => "42"
685show 42 => "42"
734show "hi" => "\"hi\""
686show "hi" => "\"hi\""
@@ -744,7 +696,7 @@ Functions render readably:
744show (map) => "map"
696show (map) => "map"
745show (+) => "(+)"
697show (+) => "(+)"
746show (describe "wip") => "describe \"wip\""
698show (describe "wip") => "describe \"wip\""
747−show (\x -> x + 1) => "\\x -> x + 1"
699+show (\x -> x + 1) => "\x -> (x + 1)"
748```
700```
749
701
750Output longer than 80 columns breaks across lines (one element per line,
702Output longer than 80 columns breaks across lines (one element per line,
@@ -778,8 +730,7 @@ prev -- to the parent (crashes at the top)
778next -- to the only child (crashes if not exactly one)
730next -- to the only child (crashes if not exactly one)
779up -- like prev, the raw zipper move
731up -- like prev, the raw zipper move
780by @wqzt -- refocus on a specific commit
732by @wqzt -- refocus on a specific commit
781−tree . goto %main -- inspect main without moving the working copy
733+goto %main -- refocus on what the remote calls main
782−new . goto %main -- start a mutable child on main
783parents -- the focus's parents, as a Revset
734parents -- the focus's parents, as a Revset
784kids -- its children
735kids -- its children
785ancestors -- it and its ancestors
736ancestors -- it and its ancestors
@@ -792,7 +743,6 @@ Edits — functions `Repo -> Repo`:
792
743
793```
744```
794new -- a new empty child of the focus
745new -- a new empty child of the focus
795−save -- record file edits without changing the description
796describe "wip" -- set the focus's message
746describe "wip" -- set the focus's message
797squash -- fold the focus into its parent
747squash -- fold the focus into its parent
798abandon -- drop the focus; children join the parent
748abandon -- drop the focus; children join the parent
@@ -807,9 +757,7 @@ And the inspections:
807
757
808```
758```
809tree -- the history as a tree (a Text)
759tree -- the history as a tree (a Text)
810−status -- { id, message, labels, changed, conflicts, unrecorded }
760+status -- { id, message, labels, changed, conflicts }
811−unrecorded -- paths edited since the last recording
812−preview squash -- validate and show the resulting tree without saving
813review -- the focus's changes as one page of diffs
761review -- the focus's changes as one page of diffs
814files -- the focus's snapshot (its files)
762files -- the focus's snapshot (its files)
815commits . top -- every commit, preorder
763commits . top -- every commit, preorder
@@ -834,13 +782,10 @@ j 'tree'
834-- the second commit was a mistake; fold it into the first
782-- the second commit was a mistake; fold it into the first
835j 'squash'
783j 'squash'
836
784
837−-- squash leaves a fresh empty child; move back and rename the combined commit
785+-- rename the combined commit
838−j prev
839j 'describe "add the front end"'
786j 'describe "add the front end"'
840
787
841−-- undo the rename, the focus move, then the squash
788+-- undo the squash entirely
842−j undo
843−j undo
844j undo
789j undo
845
790
846-- move back to the first commit to fix something
791-- move back to the first commit to fix something
@@ -869,22 +814,17 @@ diff (blob "a\nb\n") (blob "a\nc\n")
869 => "@@ -1,2 +1,2 @@\n a\n-b\n+c\n"
814 => "@@ -1,2 +1,2 @@\n a\n-b\n+c\n"
870```
815```
871
816
872−`difft p a b` renders the comparison with **difftastic** when it is on `PATH`
817+`difft p a b` renders the comparison with **difftastic** (which must be on
873−(the Nix package provides it). When it is absent, the builtin returns a
818+`PATH`; the Nix package provides it). `review` uses `difft` per changed path.
874−unified diff with path headers, or a summary for binary changes. `review`
875−uses `difft` per changed path; other errors starting the tool still fail.
876
819
877`treeWith options repo` renders the history tree with an options record:
820`treeWith options repo` renders the history tree with an options record:
878
821
879```
822```
880−treeWith ({ detail = 2, margin = true, elide = false, icons = false, color = "auto", lanes = 4, author = false, date = false, files = false })
823+treeWith ({ detail = 2, margin = true, elide = false, icons = false, color = "auto" })
881```
824```
882
825
883−The reference config provides `tree`, `treeCompact` (folded runs and distant
826+The reference config gives two ready-made versions: `tree` (compact) and
884−subtrees), `treeFull` (ages and authors in the margin), and `treeData` (extra
827+`treeFull` (with ages and authors in the margin).
885−date, files-changed, and author columns). Branches beyond `lanes` keep their
886−state marker beside `»`; increase `lanes` to show more rails. Long file paths
887−in detail rows wrap to fit the terminal.
888
828
889---
829---
890
830
@@ -960,14 +900,8 @@ the algebra they are.
960
900
961**Text:** `splitOn` `startsWith` `endsWith` `concat` `++`
901**Text:** `splitOn` `startsWith` `endsWith` `concat` `++`
962
902
963−**Blobs/repos:** `blob` `blobHex` `text` `unresolved` `replay` `by` `meta` `extract`
903+**Blobs/repos:** `blob` `text` `unresolved` `replay` `by` `meta` `extract`
964`diff` `difft` `treeWith` `validate`
904`diff` `difft` `treeWith` `validate`
965
905
966−**Reserved commands** (not expressions): `help [NAME]` `commands [QUERY]`
906+**Reserved commands** (not expressions): `init` `clone URL [DIR]` `remote URL`
967−`setup [NAME EMAIL]` `init` `clone URL [DIR]` `remote URL`
968`fetch` `push EXPR` `undo` `redo` `ops`.
907`fetch` `push EXPR` `undo` `redo` `ops`.
969−
970−Literal reserved-command arguments preserve shell quoting boundaries, as in
971−`j clone URL 'my project'`. A whole reserved command passed as one string or
972−on stdin is split on whitespace. Expression arguments still use language
973−syntax: quote the whole expression, as in `j 'describe "my change"'`.
docs/tutorial.md+36−102
@@ -4,17 +4,9 @@ This is a guided first hour with `j`. It builds the mental model piece by
4piece — not just *what* to type, but *why it works* — so that by the end you
4piece — not just *what* to type, but *why it works* — so that by the end you
5can compose your own commands instead of memorizing them.
5can compose your own commands instead of memorizing them.
6
6
7−You'll want `j` installed (`nix build` or `cargo build --release`). Set up
7+You'll want `j` installed (`nix build` or `cargo build --release`) and a
8−your configuration with your own name and email:
8+`config.j` with your identity (see the README's Configuration section). Every
9−
10−```
11−j setup 'Ada Lovelace' ada@example.org
12−```
13−
14−This creates the full reference configuration without overwriting an existing
15−file. If you already have a config, `j help config` shows its location so you
16−can edit the `user` definition. The remaining commands run in a real
17−repository, so you can follow along.
9+command shown here is run in a real repository, so you can follow along.
18
10
19---
11---
20
12
@@ -28,11 +20,11 @@ j init
28```
20```
29
21
30`j init` makes a colocated `.jj`/`.git` repository and checks out a fresh
22`j init` makes a colocated `.jj`/`.git` repository and checks out a fresh
31−working-copy commit: the commit you are currently editing. It already exists,
23+working-copy commit. Now write a file and start your first change:
32−so write a file and describe that commit:
33
24
34```
25```
35echo 'fn main() {}' > main.rs
26echo 'fn main() {}' > main.rs
27+j 'new'
36j 'describe "start the project"'
28j 'describe "start the project"'
37```
29```
38
30
@@ -44,48 +36,35 @@ j 'tree'
44
36
45```
37```
46 ⌂ zzzz
38 ⌂ zzzz
39+ └─ ● pnyz ▂
47−▶ └─ ◉ pnyz ▂ start the project
40+▶ └─ ◌ psnq start the project
48```
41```
49
42
50Add a second change on top:
43Add a second change on top:
51
44
52```
45```
53−j 'new'
54echo 'lib' > lib.rs
46echo 'lib' > lib.rs
47+j 'new'
55j 'describe "add a library"'
48j 'describe "add a library"'
56j 'tree'
49j 'tree'
57```
50```
58
51
59```
52```
60 ⌂ zzzz
53 ⌂ zzzz
54+ └─ ● pnyz ▂
61− └─ ● pnyz ▂ start the project
55+ └─ ● psnq ▂ start the project
62−▶ └─ ◉ psnq ▂ add a library
56+▶ └─ ◌ trtq add a library
63```
57```
64
58
65−That's the basic loop: **edit files, describe the current change, then `new`
59+That's the whole basic loop: **edit files, `new`, `describe`.** There is no
66−before editing the next change.** `new` opens an empty change with the same
60+`git add` step — the working directory is always already a commit, and `j`
67−files as its parent; it does not move existing edits into that new change.
61+snapshots it on every run.
68−There is no `git add` step. `describe` saves both the current files and their
69−description; `j save` records file edits without changing the description.
70
62
71−**What just happened, in the model:** each expression snapshots your working
63+**What just happened, in the model:** each `j` run snapshots your working
72−directory for evaluation, builds a `Repo` value focused on that commit,
64+directory into the working-copy commit, builds a `Repo` value focused there,
73evaluates your expression, and — because `new` and `describe` are *edits*
65evaluates your expression, and — because `new` and `describe` are *edits*
74(functions `Repo -> Repo`) — persists the resulting repository.
66(functions `Repo -> Repo`) — persists the resulting repository.
75
67
76−`j status` has two useful lists: `in commit` compares the current commit with
77−its parent, while `unrecorded` lists file edits made since the last save.
78−After `describe` or `save`, `unrecorded` is empty; `in commit` still shows the
79−work in the commit. Inspecting status does not save anything.
80−
81−To join an existing project instead, clone it and enter its directory:
82−
83−```
84−j clone URL project
85−cd project
86−j status
87−```
88−
89---
68---
90
69
91## Part 2 — The one idea everything rests on
70## Part 2 — The one idea everything rests on
@@ -154,28 +133,22 @@ function produces, so don't wrap it.
154Make a few commits to play with:
133Make a few commits to play with:
155
134
156```
135```
157−j new; echo a > a.txt; j 'describe "A"'
136+echo a > a.txt; j 'new'; j 'describe "A"'
158−j new; echo b > b.txt; j 'describe "B"'
137+echo b > b.txt; j 'new'; j 'describe "B"'
159−j new; echo c > c.txt; j 'describe "C"'
138+echo c > c.txt; j 'new'; j 'describe "C"'
160```
139```
161
140
162−Navigation changes the working-copy commit and checks out its files. Use
141+Navigation is edits too:
163−these commands to edit another mutable commit:
164
142
165```
143```
166j 'prev' -- to the parent
144j 'prev' -- to the parent
167j 'next' -- to the only child
145j 'next' -- to the only child
146+j 'top' -- to the top of history
168j 'tip' -- to the end of the current line
147j 'tip' -- to the end of the current line
169j 'by @vqok' -- to a specific commit, by id (use a real id from j tree)
148j 'by @vqok' -- to a specific commit, by id (use a real id from j tree)
149+j 'goto %main' -- to what the remote calls main
170```
150```
171
151
172−At a fork, choose an id from `j tree` with `j 'by @id'`; `next` only works
173−when there is one child. Main and the root commit are protected from editing.
174−Use `j 'new . goto trunk'` to start work on main, or
175−`j 'tree . goto trunk'` to inspect it without moving your working copy.
176−In a new local repository with no main bookmark, keep working from `j init`'s
177−commit instead.
178−
179After `j 'prev'` from C, the tree shows the focus (▶ and ◉) on B:
152After `j 'prev'` from C, the tree shows the focus (▶ and ◉) on B:
180
153
181```
154```
@@ -191,16 +164,11 @@ To see the focus's data, use the inspection functions:
191```
164```
192j 'files' -- the focus's files
165j 'files' -- the focus's files
193j 'changed' -- paths that differ from the parent
166j 'changed' -- paths that differ from the parent
194−j 'status' -- focus, changes in the commit, unrecorded edits, conflicts
167+j 'status' -- id, message, labels, changed, conflicts
195j 'log' -- a table of every commit, ✓ on the focus
168j 'log' -- a table of every commit, ✓ on the focus
196j 'review' -- the focus's changes as diffs (difftastic)
169j 'review' -- the focus's changes as diffs (difftastic)
197```
170```
198
171
199−These inspections include current file edits without recording them. Use
200−`j save` to record file edits on their own (`id` is the same edit). `status` spells out empty states
201−such as `No changes` and `No conflicts`; `log` shows label names and short
202−ids and adapts to narrow terminals.
203−
204### Composition order, made concrete
172### Composition order, made concrete
205
173
206```
174```
@@ -226,10 +194,11 @@ operates on the repository the previous step produced.
226
194
227## Part 4 — Dry runs: seeing before doing
195## Part 4 — Dry runs: seeing before doing
228
196
229−Use `preview` to validate an edit and show its resulting tree without saving:
197+Because inspections persist nothing, you can preview any edit by composing it
198+with `tree`:
230
199
231```
200```
232−j 'preview squash'
201+j 'tree . squash'
233```
202```
234
203
235From the A → B history, this *shows* the tree as `squash` would leave it (B
204From the A → B history, this *shows* the tree as `squash` would leave it (B
@@ -252,21 +221,18 @@ because `tree` returns a `Text`, not a `Repo`:
252▶ └─ ◌ nknz B
221▶ └─ ◌ nknz B
253```
222```
254
223
255−`preview` combines `tree` and `validate`. For an edit that takes arguments,
224+For a stronger guarantee, insert `validate`. `validate repo` returns the repo
256−put that edit in parentheses:
225+unchanged if persisting would succeed, and crashes with the exact error
226+otherwise:
257
227
258```
228```
259−j 'preview (rebase %main)'
229+j 'tree . validate . rebase %main'
260```
230```
261
231
262This renders the post-rebase tree **and** crashes (without touching anything)
232This renders the post-rebase tree **and** crashes (without touching anything)
263if that rebase couldn't actually happen — for instance, if `%main` doesn't
233if that rebase couldn't actually happen — for instance, if `%main` doesn't
264exist yet. That's an *honest* dry run.
234exist yet. That's an *honest* dry run.
265
235
266−The longer form is `j 'tree . validate . rebase %main'`. `validate` alone
267−does not make an edit a dry run: `j 'validate . squash'` returns a repository
268−and saves it. The final inspection (`tree`) is what prevents persistence.
269−
270---
236---
271
237
272## Part 5 — `or`: the whole error-handling model
238## Part 5 — `or`: the whole error-handling model
@@ -324,7 +290,7 @@ Two things make these safe to use freely:
324
290
3251. **The change id survives.** A commit keeps its `@wqzt…` id across
2911. **The change id survives.** A commit keeps its `@wqzt…` id across
326 `describe`, `squash`, `rebase`. You're never tracking a moving sha.
292 `describe`, `squash`, `rebase`. You're never tracking a moving sha.
327−2. **Saved operations are undoable.** Use `j undo` and `j redo` to walk them.
293+2. **`j undo` always works.** Every operation is recorded.
328
294
329```
295```
330j undo -- revert the last thing
296j undo -- revert the last thing
@@ -332,28 +298,6 @@ j redo -- reapply it
332j ops -- see the operation log
298j ops -- see the operation log
333```
299```
334
300
335−Undo and redo stop when files have unrecorded edits. Recording those edits
336−with `j id` adds an operation, so the next undo removes that recording;
337−another undo reaches the preceding edit. Recording also clears pending
338−redos. Read `j ops` to choose how far to go back. Successful interactive
339−undo/redo messages identify the affected operation and restored focus.
340−
341−### Resolve a conflict
342−
343−A successful rebase can leave conflicts in the working-copy commit. The edit
344−is saved; there is no separate continuation command. Resolve it in four steps:
345−
346−1. Run `j status` to find conflicted paths, and `j review` to inspect changes.
347−2. Edit those files, choosing the intended content and removing conflict markers.
348−3. Run `j save` to record the resolution.
349−4. Run `j status` again and check for `No conflicts` and `All edits recorded`.
350−
351−`j conflicts` lists conflicted commits throughout the repository. If a
352−conflict is on another mutable commit, use its id from `j tree` with
353−`j 'by @id'`, then follow the same steps. Run `j conflicts` again to check
354−the whole history. `review` uses difftastic when
355−installed and falls back to a unified diff when it is unavailable.
356−
357### `split` and `contract`: moving changes by fileset
301### `split` and `contract`: moving changes by fileset
358
302
359A **fileset** is a function `Path -> Bool`. These two are inverses:
303A **fileset** is a function `Path -> Bool`. These two are inverses:
@@ -397,12 +341,9 @@ Most commands that take a "which commits" argument take a revset. `goto` is
397the one place a revset must name *exactly one* commit:
341the one place a revset must name *exactly one* commit:
398
342
399```
343```
400−j 'new . goto %main' -- select one main commit and start a mutable child
344+j 'goto %main' -- crash if %main names 0 or 2+ commits
401```
345```
402
346
403−A missing label reports how to fetch and inspect remote labels. An ambiguous
404−selector lists candidate ids and descriptions so you can choose with `by`.
405−
406Revsets combine like sets:
347Revsets combine like sets:
407
348
408```
349```
@@ -446,9 +387,9 @@ Everything `j` knows is defined in `config.j`. Any definition whose value is
446an `Edit` is a new command. Add this to your `~/.config/j/config.j`:
387an `Edit` is a new command. Add this to your `~/.config/j/config.j`:
447
388
448```
389```
449−-- squash into the parent and name the combined commit "release"
390+-- fold the whole stack into one commit called "release"
450release : Edit
391release : Edit
451−release = describe "release" . prev . squash
392+release = describe "release" . (contract everything)
452```
393```
453
394
454Now `j release` works exactly like `squash` or `new`. The signature (`: Edit`)
395Now `j release` works exactly like `squash` or `new`. The signature (`: Edit`)
@@ -491,8 +432,8 @@ echo 'fn parse() {}' > parser.rs
491j 'describe "add the parser"'
432j 'describe "add the parser"'
492
433
493# second part
434# second part
494−j 'new'
495echo 'mod parser;' > lib.rs
435echo 'mod parser;' > lib.rs
436+j 'new'
496j 'describe "wire up the parser"'
437j 'describe "wire up the parser"'
497
438
498# look at it
439# look at it
@@ -501,24 +442,17 @@ j 'tree'
501# the second commit was really part of the first; fold it in
442# the second commit was really part of the first; fold it in
502j squash
443j squash
503
444
504−# squash leaves an empty child focused; move to the combined commit
505−j prev
506−
507# rename the combined commit
445# rename the combined commit
508j 'describe "add the parser"'
446j 'describe "add the parser"'
509
447
510# preview the rebase, then do it
448# preview the rebase, then do it
511−j 'preview (rebase %main)'
449+j 'tree . validate . rebase %main'
512j 'rebase %main'
450j 'rebase %main'
513
451
514# share it
452# share it
515j 'push (label "parser" here)'
453j 'push (label "parser" here)'
516```
454```
517
455
518−Push sends recorded commits. If you edit files after the last saved edit,
519−push warns that those edits are omitted. Run `j save`, then push again to
520−include them. Looking at `j status` does not save the edits.
521−
522And if any step goes wrong:
456And if any step goes wrong:
523
457
524```
458```
specs/main.md+198−948
@@ -9,7 +9,7 @@ Nothing is implicit beyond the language itself. Every shape, builtin, and
9command is declared or defined in one user-owned file, `~/.config/j/config.j`,
9command is declared or defined in one user-owned file, `~/.config/j/config.j`,
10written in the language. The binary provides the language (its syntax, its
10written in the language. The binary provides the language (its syntax, its
11operators and their fixities, its primitive kinds), the implementations of the
11operators and their fixities, its primitive kinds), the implementations of the
12−declared builtins, eleven reserved words, and the bridge to jj repositories.
12+declared builtins, eight reserved words, and the bridge to jj repositories.
13`j` does not require the `jj` binary; it uses jj's on-disk formats through
13`j` does not require the `jj` binary; it uses jj's on-disk formats through
14jj-lib.
14jj-lib.
15
15
@@ -40,37 +40,21 @@ echo EXPRESSION | j
40 `::`, `or`, `.`, and the literals `%name`, `@id`, `./a/b` are all safe
40 `::`, `or`, `.`, and the literals `%name`, `@id`, `./a/b` are all safe
41 unquoted.
41 unquoted.
423. If arguments are present and rule 1 also applies, exit 2 with
423. If arguments are present and rule 1 also applies, exit 2 with
43− `j: expression given both as arguments and on stdin`. One byte is
43+ `j: expression given both as arguments and on stdin`.
44− sufficient to reject the invocation; the pipe need not reach EOF.
45−4. If neither is present, or the input is only whitespace, print a concise
44+4. If neither is present, print a one-line usage message to stderr and exit 2.
46− usage and getting-started guide to stderr and exit 2. The guide points to
45+5. `j` accepts no flags. Any argument, including ones beginning with `-`, is
47− `j help`, `j commands`, and commands for starting or inspecting a repository.
46+ part of the expression.
48− It does not read or create configuration or open a repository.
49−5. `--help` and `-h` as the first word alias `help` (§1.1). A separate
50− argument equal to either flag after the first argument requests help for
51− the first word, before repository access or evaluation: `j abandon --help`
52− is `j help abandon`. An initial `help`, `--help`, or `-h` followed by
53− another help flag opens general help. Other flags are not supported.
54− Language comments embedded within a single argument or stdin are unchanged:
55− `j 'new --help'` executes `new`, and `j '"--help"'` prints text.
56−6. The expression is text: an argument that is not valid UTF-8 is a usage
57− error (exit 2) naming it, and so is an expression on standard input that
58− is not.
59
47
60### 1.1 Reserved commands
48### 1.1 Reserved commands
61
49
62Before the expression is parsed, its text is trimmed of leading and trailing
50Before the expression is parsed, its text is trimmed of leading and trailing
63whitespace and split on whitespace into words. If the first word is one of the
51whitespace and split on whitespace into words. If the first word is one of the
64−eleven reserved words below, the whole text is a reserved command. A first word
52+eight reserved words below, the whole text is a reserved command.
65−of `--help` or `-h` is treated as `help`.
66
53
67| command | operation |
54| command | operation |
68|---|---|
55|---|---|
69−| `help [NAME]` | show the command guide, or help for a reserved command or config name; §1.1.1 |
70−| `commands [QUERY]` | show common tasks, search the vocabulary, or use `all` for the full catalog; §1.1.1 |
71−| `setup [NAME EMAIL]` | create the complete reference configuration, optionally setting identity; §6.1.1 |
72| `init` | create a repository in the current directory; §7.8 |
56| `init` | create a repository in the current directory; §7.8 |
73−| `clone URL [DIR]` | clone `URL` into `DIR` (default: the repository's name in `URL`); §7.8 |
57+| `clone URL [DIR]` | clone `URL` into `DIR` (default: last path component of `URL`, minus `.git`); §7.8 |
74| `remote URL` | set the URL of the remote `origin`, creating it if absent; §7.8 |
58| `remote URL` | set the URL of the remote `origin`, creating it if absent; §7.8 |
75| `fetch` | fetch from `origin`; §7.6 |
59| `fetch` | fetch from `origin`; §7.6 |
76| `push EXPR` | set or delete the remote bookmarks that `EXPR` selects; §7.6 |
60| `push EXPR` | set or delete the remote bookmarks that `EXPR` selects; §7.6 |
@@ -78,64 +62,24 @@ of `--help` or `-h` is treated as `help`.
78| `redo` | reverse the newest undo; §7.7 |
62| `redo` | reverse the newest undo; §7.7 |
79| `ops` | print the operation log; §7.7 |
63| `ops` | print the operation log; §7.7 |
80
64
81−For every reserved command except `push` and `commands`, the remaining words
65+For every reserved command except `push`, the remaining words are literal
82−are literal arguments, and the language is not involved. When the first
66+arguments (no quoting, no escaping); a wrong number is a usage error (exit 2)
83−shell argument is the command name, the remaining shell argument boundaries
67+and the language is not involved. For `push`, the remainder of the text is an
84−are preserved, so `j clone '/path/to/source repo' 'my project'` accepts both
85−paths. A command supplied as one string or on stdin is split on whitespace;
86−it has no additional quote or escape syntax. A wrong argument count is a
87−usage error (exit 2). `commands` takes the remaining
88−trimmed text as its optional search query, including spaces. For `push`, the
89−remainder of the text is an expression, evaluated as in §1.2 steps 2–7 but
68+expression, evaluated as in §1.2 steps 2–7 but against the repository as
90−against the repository as recorded, without snapshotting the working directory;
69+recorded, without snapshotting the working directory; its value must be a list
91−its value must be a list of push records (§7.6).
70+of push records (§7.6).
92
71
93Matching is on the first word only, so an expression whose first token is one
72Matching is on the first word only, so an expression whose first token is one
94of these words cannot be written on the command line. `(push)` or `x fetch`
73of these words cannot be written on the command line. `(push)` or `x fetch`
95are ordinary expressions. `config.j` may define functions with these names;
74are ordinary expressions. `config.j` may define functions with these names;
96they are reachable only inside larger expressions.
75they are reachable only inside larger expressions.
97
76
98−#### 1.1.1 Help and command discovery
99−
100−`help` without a name prints a getting-started guide to stdout: repository
101−setup, common commands grouped by task, examples, expression quoting and
102−composition, dry runs, and how to find more commands. `help NAME` prints help
103−for that reserved command or for a name in the current configuration, including
104−its signature when present and available documentation. `--help [NAME]` and
105−`-h [NAME]` are equivalent to `help [NAME]`. These successful requests exit 0.
106−An unknown name is a usage error (exit 2) with guidance to use `j commands`.
107−`j help config` is a built-in topic showing the configuration path, identity
108−setup, and repair instructions. It is available even with invalid configuration.
109−
110−`commands` shows a concise catalog of common tasks and their commands.
111−`commands all` lists the reserved commands and vocabulary actually declared
112−or defined in the current configuration, including custom definitions without
113−signatures. The concise catalog includes a bounded selection of custom
114−definitions and points to the full list. `commands QUERY` searches the catalog case-insensitively by name,
115−signature, documentation comments, and common task synonyms such as `commit`,
116−`checkout`, and `branch`, ranking direct matches and everyday commands first.
117−The `stash` query explains the save/new workflow when no custom `stash`
118−definition exists. The catalog does not present an
119−undeclared builtin as available merely because the binary implements it.
120−
121−General help and help for reserved commands do not depend on configuration and
122−remain available if it is unreadable or invalid. Discovery of configuration
123−names reads the configured file (§6.1) without creating or modifying it. If the
124−file is missing, it uses the embedded reference configuration; other read or
125−parse errors exit 3 rather than silently substituting a different catalog.
126−The config is never evaluated for discovery, so its definitions can refer to
127−repository state without preventing their documentation from being read.
128−
129−Neither command locates or opens a repository, takes a repository lock,
130−resolves commit ids, snapshots the working directory, or records operations.
131−
132### 1.2 Evaluating the expression
77### 1.2 Evaluating the expression
133
78
1341. Locate the repository (§7.1). If none, exit 2.
791. Locate the repository (§7.1). If none, exit 2.
1352. Parse the expression (§3). On parse error, exit 3.
802. Parse the expression (§3). On parse error, exit 3.
1363. Load and validate `config.j` (§6), except for its `Id` literals. If it is
813. Load and validate `config.j` (§6), except for its `Id` literals. If it is
137− missing, create the reference configuration (§6.1); if loading or
82+ missing or invalid, exit 3.
138− validation fails, exit 3.
1394. Build the current repository value `r` (§7.2). This includes snapshotting
834. Build the current repository value `r` (§7.2). This includes snapshotting
140 the working directory into the focused commit (§7.4).
84 the working directory into the focused commit (§7.4).
1415. Resolve every `Id` literal in `config.j` and in the expression (§4.10).
855. Resolve every `Id` literal in `config.j` and in the expression (§4.10).
@@ -146,8 +90,7 @@ resolves commit ids, snapshots the working directory, or records operations.
1467. If `v` is a function, apply it to `r`, once, and let `v` be the result.
907. If `v` is a function, apply it to `r`, once, and let `v` be the result.
1478. If `v` is a `Repo` (§4.4): if it equals the repository value *as loaded,
918. If `v` is a `Repo` (§4.4): if it equals the repository value *as loaded,
148 before the snapshot of §7.4*, do nothing at all; otherwise persist it
92 before the snapshot of §7.4*, do nothing at all; otherwise persist it
149− (§7.5) and check out its focused commit's files (§7.4). Print nothing to stdout;
93+ (§7.5) and check out its focused commit's files (§7.4). Print nothing.
150− interactive stderr reports the outcome and focus (§1.5).
151 Exit 0.
94 Exit 0.
1529. Otherwise display `v` (§5.1) on stdout. Persist nothing. Exit 0.
959. Otherwise display `v` (§5.1) on stdout. Persist nothing. Exit 0.
153
96
@@ -157,7 +100,7 @@ persist nothing, leave the working directory untouched, and exit 1.
157Step 7 applies exactly once. If the result of applying a function to `r` is
100Step 7 applies exactly once. If the result of applying a function to `r` is
158again a function, that function is displayed, not applied again.
101again a function, that function is displayed, not applied again.
159
102
160−Step 8's equality test means `j save` (the reference alias for `id`) is a no-op when the working directory is
103+Step 8's equality test means `j id` is a no-op when the working directory is
161unchanged and records it when it is not: the snapshot makes `r` differ from
104unchanged and records it when it is not: the snapshot makes `r` differ from
162the loaded value, and that difference is what gets persisted. The test may be
105the loaded value, and that difference is what gets persisted. The test may be
163implemented by comparing commit and tree hashes rather than values.
106implemented by comparing commit and tree hashes rather than values.
@@ -169,34 +112,16 @@ Two consequences worth knowing:
169 what a rebase would produce without doing it. `validate` in the chain,
112 what a rebase would produce without doing it. `validate` in the chain,
170 `j tree . validate . rebase trunk`, additionally raises whatever
113 `j tree . validate . rebase trunk`, additionally raises whatever
171 persistence would refuse, so the preview is of something that could
114 persistence would refuse, so the preview is of something that could
172− actually be persisted. The reference `preview EDIT` is the convenient
115+ actually be persisted.
173− form of `tree . validate . EDIT`: `j 'preview (rebase trunk)'`. Validation
116+- **Do, then look.** `j squash` prints nothing. `j squash && j tree` is the
174− alone returns a `Repo`, so `validate . EDIT` persists a changed result.
175−- **Do, then look.** `j squash` reports the resulting focus on an interactive
176− terminal. `j squash && j tree` displays the whole tree; there is no flag for it.
117+ idiom; there is no flag for it.
177
118
178### 1.3 Interrupts
119### 1.3 Interrupts
179
120
180There is no timeout. A non-terminating expression runs until interrupted.
121There is no timeout. A non-terminating expression runs until interrupted.
181−Interrupting `j` (SIGINT) kills it, as SIGTERM, SIGHUP and SIGQUIT do.
122+Interrupting `j` (SIGINT) kills it; because nothing is written until
182−Nothing is written until the operation is recorded (§7.5 step 7), so a run
183−interrupted before then persists nothing and leaves the working directory
123+evaluation completes, an interrupted run persists nothing and leaves the
184−untouched. From the recording until the focus is checked out and the
124+working directory untouched.
185−working copy's state is saved, those four signals wait: the checkout
186−completes, the run reports what it did, and then the signal takes effect as
187−it would have. A run that ends in that window regardless (SIGKILL, a crash,
188−a power loss) leaves the operation recorded and the working directory
189−partly updated, as a checkout that fails does: the next run carries on from
190−there (§7.4), and `j undo` goes back.
191−
192−`clone` (§7.8) writes its directory from the start, so it holds the four
193−signals off from creating it. One that comes while it creates the
194−repository, fetches and imports lets the step in progress end (the git it
195−runs, which a Ctrl-C at the terminal stops too), then fails the clone, which
196−removes what it made, and then takes effect; one that comes later, once it
197−makes the working-copy commit, waits for the clone to complete. A signal `j`
198−was started ignoring (`nohup`, a background job) stays ignored, by the git
199−it runs too.
200
125
201### 1.4 Exit status
126### 1.4 Exit status
202
127
@@ -207,67 +132,10 @@ it runs too.
207| 2 | usage error: bad arguments, no repository, unsupported repository (§7.2), another `j` running (§7.7) |
132| 2 | usage error: bad arguments, no repository, unsupported repository (§7.2), another `j` running (§7.7) |
208| 3 | configuration or parse error |
133| 3 | configuration or parse error |
209
134
210−If standard output is a pipe whose reader has exited (`j tree | head -1`), the
211−rest of the output is dropped and `j` exits 0; any other failure to write
212−output is an error, status 1. A message that cannot be written to stderr, for
213−whatever reason, is dropped and the run keeps its status:
214−`j 'crash "boom"' 2>&1 | true` still exits 1.
215−
216Errors are written to stderr as a single line beginning with `j: `. Crash
135Errors are written to stderr as a single line beginning with `j: `. Crash
217messages are the text given to `crash` (or generated by the interpreter),
136messages are the text given to `crash` (or generated by the interpreter),
218prefixed with `j: crash: `, followed on a second line by the innermost base
137prefixed with `j: crash: `, followed on a second line by the innermost base
219−definition that was executing and the top-level expression. A definition is
138+definition that was executing and the top-level expression.
220−executing while the body of its lambda runs, whether it has a signature or
221−not, and one with a signature also while its value runs when that is a
222−builtin (`squash`) and while an argument or result of it is checked (§4.13),
223−so `describe 3` is in `describe`. A lambda's body is its own definition's
224−under any other name: `prev = goto parents` runs goto's body, so `prev . top`
225−is in `goto`. A builtin is not a definition, so a crash in one is in
226−whichever definition applied it, and with none executing the line is only
227−` from <expression>`.
228−
229−### 1.5 Terminal presentation
230−
231−Automatic colors are used only on a terminal, and disabled by `TERM=dumb`
232−or a nonempty `NO_COLOR`. Diagnostics use the stderr terminal; displayed
233−values use stdout. Error prefixes are bright red, informational prefixes
234−bright cyan, and interactive successful commands have a green checkmark.
235−Color adds emphasis without replacing the text of errors or values.
236−Successful edits report `Saved changes` or `No changes`, followed by the
237−resulting focus's copyable, unique `@` prefix and first description line
238−(`(no description)` when empty). Setup reports the effective author identity.
239−Undo and redo identify the affected operation and restored focus.
240−After a saved edit, conflicts newly introduced anywhere in the history and
241−conflicts in the checked-out focus produce a warning with counts, paths, and
242−resolution guidance. This warning remains visible when output is redirected;
243−the operation still succeeds. Guidance identifies `j conflicts`, editing
244−conflict markers, recording the resolution with `j save` or `j id`, and
245−checking again. For another commit, navigate with `by` first.
246−
247−When both stdout and stderr are capable terminals, repository operations,
248−configuration evaluation, and expression evaluation (including implicit application)
249−may show a temporary, color-cycling progress line on stderr. It starts after
250−140 ms, advances every 75 ms, and names the current operation alongside a
251−spinner, a bouncing activity bar, and elapsed time since that operation began.
252−The bar is indeterminate and does not estimate completion. Each frame fits
253−the current stderr terminal width with a spare column, omitting the bar and
254−then the time before truncating the operation's name by display width. The
255−line is cleared before the command reports its result.
256−Fast operations never wait for a progress frame. It does not hide
257−the cursor or change terminal modes. Signal behavior remains as in §1.3.
258−Before an external renderer inherits stderr, progress is cleared and stopped
259−for the remainder of the run so even an unterminated diagnostic stays visible.
260−
261−Colored multiline output is revealed in at most seven batches, with at
262−most 96 ms of total pacing. Its bytes and row order are unchanged. Plain
263−text is written directly. `J_ANIMATIONS=never` (also `off`, `false`, or `0`,
264−case-insensitive) disables all motion while retaining colors. Motion is
265−also disabled by nonempty `NO_COLOR` or `CI`, `TERM=dumb`, or either output
266−stream being redirected. Success messages are omitted only when `CI` is
267−nonempty or either output stream is redirected. `NO_COLOR` and `TERM=dumb`
268−retain plain-text success messages on terminals; `J_ANIMATIONS` alone does
269−not suppress them.
270−Broken pipes and failed writes keep the behavior in §1.4.
271
139
272---
140---
273
141
@@ -299,8 +167,7 @@ same value if they have the same field names with equal values.
299
167
300### 3.1 Lexical structure
168### 3.1 Lexical structure
301
169
302−- **Comments.** `--` to end of line. `{-` … `-}`, nesting. A comment is
170+- **Comments.** `--` to end of line. `{-` … `-}`, nesting.
303− whitespace: a block comment ends a line (§3.3) only by containing a line break.
304- **Identifiers.** `[a-z_][A-Za-z0-9_']*`. Keywords are excluded:
171- **Identifiers.** `[a-z_][A-Za-z0-9_']*`. Keywords are excluded:
305 `let in if then else or true false`.
172 `let in if then else or true false`.
306- **Type names.** `[A-Z][A-Za-z0-9_]*`. Declared as shapes in `config.j`;
173- **Type names.** `[A-Z][A-Za-z0-9_]*`. Declared as shapes in `config.j`;
@@ -356,7 +223,7 @@ keyword and is always available.
356
223
357### 3.3 Layout
224### 3.3 Layout
358
225
359−Three layout rules and nothing else.
226+Two layout rules and nothing else.
360
227
3611. **Top level.** A top-level item begins with a token in column 1. Every
2281. **Top level.** A top-level item begins with a token in column 1. Every
362 following line whose first token is in a column greater than 1 continues
229 following line whose first token is in a column greater than 1 continues
@@ -364,22 +231,12 @@ Three layout rules and nothing else.
3642. **`let` blocks.** After `let`, the column of the first token opens a block.
2312. **`let` blocks.** After `let`, the column of the first token opens a block.
365 A subsequent line whose first token is in exactly that column begins a new
232 A subsequent line whose first token is in exactly that column begins a new
366 binding; a line indented further continues the current binding; the block
233 binding; a line indented further continues the current binding; the block
367− ends at `in` or at the first token in a smaller column. An `in` that begins
234+ ends at `in` or at the first token in a smaller column. Bindings on a single
368− a line obeys the enclosing layout like any other token: in an item it must
369− be past column 1, and in an outer block's binding past that block's column.
370− Bindings on a single line are separated by `;`. Blank and comment-only
235+ line are separated by `;`. Blank and comment-only lines are ignored here
371− lines are ignored here too.
236+ too.
372−3. **Selectors and updates.** A `SELECTOR` or `{` that is the first token of
373− a line never selects from or updates what precedes it. On a continuation
374− line it begins a new `postfix` (§3.4), which after another `postfix` is
375− the next application argument or list element: `r` followed by a line
376− ` .a` is `r (.a)`, and followed by ` { a = 2 }` it is `r ({ a = 2 })`.
377− So `r.a` and `r { a = 2 }` need the `.a` or `{` on the line `r` ends on.
378− `show` relies on this to break a list of records one per line with nothing
379− between them (§5.2).
380
237
381An expression given on the command line or on stdin is a single `expr`, not a
238An expression given on the command line or on stdin is a single `expr`, not a
382−list of items, so rule 1 does not apply to it; rules 2 and 3 do.
239+list of items, so rule 1 does not apply to it; rule 2 does.
383
240
384### 3.4 Grammar
241### 3.4 Grammar
385
242
@@ -405,7 +262,7 @@ binding := ident '=' expr
405
262
406opexpr := app (op app)* -- resolved by the fixity table
263opexpr := app (op app)* -- resolved by the fixity table
407app := postfix+ -- juxtaposition; left-associative
264app := postfix+ -- juxtaposition; left-associative
408−postfix := atom ( SELECTOR | '{' fields '}' )* -- no SELECTOR or '{' after a line break (§3.3)
265+postfix := atom ( SELECTOR | '{' fields '}' )*
409atom := ident | TYPENAME | INT | TEXT | IDLIT | NEWID | LABELLIT | PATHLIT | 'true' | 'false'
266atom := ident | TYPENAME | INT | TEXT | IDLIT | NEWID | LABELLIT | PATHLIT | 'true' | 'false'
410 | '(' expr ')' | '(' op ')' | '(' op expr ')' | '(' expr op ')'
267 | '(' expr ')' | '(' op ')' | '(' op expr ')' | '(' expr op ')'
411 | '[' postfix* ']'
268 | '[' postfix* ']'
@@ -433,23 +290,9 @@ Notes:
433 a `postfix` (an atom with selectors and updates): `[1 2 3]`,
290 a `postfix` (an atom with selectors and updates): `[1 2 3]`,
434 `["src" "lexer.rs"]`, `[{ root = c, children = [] }]`. An application or
291 `["src" "lexer.rs"]`, `[{ root = c, children = [] }]`. An application or
435 operator expression as an element is parenthesised: `[(up repo).root.id]`.
292 operator expression as an element is parenthesised: `[(up repo).root.id]`.
436− So is a record literal after another element, `[({ a = 1 }) ({ a = 2 })]`:
437− `[{ a = 1 } { a = 2 }]` is the one-element list `[{ a = 2 }]`, the first
438− record updated by the second. Records keep commas.
293+ Records keep commas.
439- Lambda, `if`, `let`, and `or` extend as far right as possible.
294- Lambda, `if`, `let`, and `or` extend as far right as possible.
440- Patterns are variables and `_` only. No literal, list, or record patterns.
295- Patterns are variables and `_` only. No literal, list, or record patterns.
441−- Nesting is bounded: an expression more than 40,000 levels deep is a parse
442− error (§1.4), never a stack overflow. A name, literal, `(op)` or `.name`
443− accessor is one level; a parenthesis or section, and the braces of a record
444− or update, are two levels above what they enclose; every other construct
445− is one level above its highest part. A chain `a + b + c …`, `f x y …` or
446− `r.a.b …` is thus one level deeper per link, as are right-associative
447− operators, `or`, and `\`/`if`/`let` bodies. Lists nest 40,000 deep,
448− parentheses and records about 20,000, and a `Subtree` as `show` renders it
449− (§5.2) about 13,000: three levels a commit, the braces of its record and
450− the list around them, as a list broken over lines holds records without
451− parentheses (§3.3). A type in `config.j` is bounded the same way: one
452− level, plus one for each bracket, parenthesis, field or `->` around it.
453
296
454---
297---
455
298
@@ -465,19 +308,8 @@ refers to outside a lambda. A cycle among such references (two non-lambda
465definitions each needing the other's value) is a configuration error;
308definitions each needing the other's value) is a configuration error;
466references inside lambda bodies never form cycles, since a lambda is a value
309references inside lambda bodies never form cycles, since a lambda is a value
467before it is applied. The bindings of a `let` block are evaluated by the same
310before it is applied. The bindings of a `let` block are evaluated by the same
468−rule, with a cycle being a crash. A top-level definition is moreover evaluated
311+rule, with a cycle being a crash. A lambda body is evaluated on each
469−after every definition that it reaches through a chain of references of
470−either kind but that does not reach it back. Among definitions that do reach
471−each other, one that refers to none of the others outside a lambda, and whose
472−every lambda outside other lambdas that refers to one of them is stored, is
473−evaluated before the rest, since it cannot apply their functions; an
474−expression is stored when it is the whole definition, or a record field, list
475−element, `if` branch or `let` body of a stored expression. So a function
476−applied at load finds the definitions its body refers to, save one that
477−reaches back to the definition applying the function and is not evaluated
478−before the rest; the order among the rest is otherwise unspecified. A `let`
479−binding is ordered the same way among the bindings of its block. A lambda
480−body is evaluated on each application.
312+application.
481Recursion is unbounded; the interpreter must handle deep recursion without
313Recursion is unbounded; the interpreter must handle deep recursion without
482stack overflow (or turn exhaustion into a crash).
314stack overflow (or turn exhaustion into a crash).
483
315
@@ -491,18 +323,9 @@ function.
491
323
492There is no shadowing. A `let` binding or lambda parameter may not use a name
324There is no shadowing. A `let` binding or lambda parameter may not use a name
493already bound in an enclosing scope, including top-level definitions and
325already bound in an enclosing scope, including top-level definitions and
494−builtins (reserved whether or not declared, §6.2); in `config.j` that is a
326+declared builtins; in `config.j` that is a configuration error (exit 3), on
495−configuration error (exit 3), on the command line a parse error. A lambda's
496−earlier parameters enclose its later ones, and all bindings of a `let` block
497−enclose each of its expressions, since they are mutually recursive (§4.1), so
498−`\x x -> x` and `let a = \b -> b; b = 2 in a 1` are errors too. The one
499−exception is within `config.j`, whose binders may reuse the names of its own
500−top-level definitions, as the reference config's `\files ->` and
501−`\old new repo ->` do: within the binder's scope the name means the binder,
502−for evaluation order (§4.1) as for lookup, so a `let` binding used before it
503−is evaluated is a crash there as anywhere, never the definition. Defining a
504−top-level name twice, or defining a name that is also a builtin, is a
327+the command line a parse error. Defining a top-level name twice, or defining a
505−configuration error.
328+name that is also a declared builtin, is a configuration error.
506
329
507### 4.3 Records
330### 4.3 Records
508
331
@@ -528,9 +351,7 @@ negation. Ordering operators are defined on `Int` only and crash otherwise.
528
351
529`if`, `or`, `&&`, and `||` do not evaluate all operands. `&&` and `||`
352`if`, `or`, `&&`, and `||` do not evaluate all operands. `&&` and `||`
530short-circuit. The interpreter recognises `&&` and `||` by name; `or` is a
353short-circuit. The interpreter recognises `&&` and `||` by name; `or` is a
531−keyword. Like other builtins, `&&` and `||` must be declared. Their contracts
354+keyword.
532−check each evaluated operand and the final result; a skipped operand is
533−neither evaluated nor checked.
534
355
535`a or b` evaluates `a`. If that crashes, the crash is discarded and `b` is
356`a or b` evaluates `a`. If that crashes, the crash is discarded and `b` is
536evaluated and returned. Otherwise, if the value of `a` is a function, `b` is
357evaluated and returned. Otherwise, if the value of `a` is a function, `b` is
@@ -596,19 +417,14 @@ as in §4.13.
596| `replay onto ch` | replay the change `ch` (`{ from, to }`) onto the snapshot `onto`; §7.3 |
417| `replay onto ch` | replay the change `ch` (`{ from, to }`) onto the snapshot `onto`; §7.3 |
597| `unresolved b` | `true` iff the blob is a conflict |
418| `unresolved b` | `true` iff the blob is a conflict |
598| `blob t` | a resolved regular-file blob with content `t` |
419| `blob t` | a resolved regular-file blob with content `t` |
599−| `blobHex kind hex` | a resolved blob of kind `"regular"`, `"executable"`, or `"symlink"`, with bytes decoded from an even-length hexadecimal text; accepts either letter case, crashes for an unknown kind or invalid hex |
600| `text b` | the content of a blob as text; for an unresolved blob, its conflict-marker rendering |
420| `text b` | the content of a blob as text; for an unresolved blob, its conflict-marker rendering |
601| `by i repo` | the repo refocused on the commit with id `i`; crashes if absent. Backed by the change-id index; its reference definition is in §10 |
421| `by i repo` | the repo refocused on the commit with id `i`; crashes if absent. Backed by the change-id index; its reference definition is in §10 |
602−| `lookupCommit i repo` | `(by i repo).root`, indexed once per repository value without constructing a location for each lookup. Duplicate ids select the first preorder match; malformed trees retain `by`'s traversal behavior |
603−| `lookupContent p s` | `(head (filter (\e -> e.path == p) s)).content or blob ""`, with an index for ordinary paths. The full filter's errors and first-match behavior are preserved; the reference `contentAt` uses this builtin |
604−| `subtreeCommits t` | every commit of the tree `t` (anything with `root` and `children`) in preorder, in time linear in its size: the value of `t.root :: (concat (map subtreeCommits t.children) or [])`. So it crashes only if `t` has no `root`, and a node whose `children` is not a list of values that have a `root` contributes only its own. The reference `commits` is this builtin (§9) |
605−| `diff a b` | a unified diff from the text of `a` to the text of `b`, split into lines after each `\n` (not at a lone `\r`), no `---`/`+++` header lines, three lines of context, each hunk headed `@@ -s,n +s,n @@` with the first line and the number of lines it shows of each side, as `diff -u` writes it, a line that has no trailing newline followed by `\ No newline at end of file`; `""` if equal; crashes if either is not UTF-8 |
422+| `diff a b` | a unified diff from the text of `a` to the text of `b`, no header lines, three lines of context; `""` if equal; crashes if either is not UTF-8 |
606−| `difft p a b` | the output of difftastic comparing `a` to `b` as the file `p`, with a unified-diff fallback when the executable is absent; §7.10 |
423+| `difft p a b` | the output of difftastic comparing `a` to `b` as the file `p`; §7.10. Crashes if `difft` is not on `PATH` |
607−| `unrecorded repo` | paths changed between the invocation's loaded and snapshotted focus files; §7.4. Returns `[]` without snapshot context, including in a push expression |
608| `treeWith o r` | the history rendered as a tree with options record `o`; §7.11 |
424| `treeWith o r` | the history rendered as a tree with options record `o`; §7.11 |
609| `extract S v` | every subvalue of `v` that is an `S`; §4.12 |
425| `extract S v` | every subvalue of `v` that is an `S`; §4.12 |
610| `meta i` | `{ hash, author, email, time }` for the visible commit with change id `i`: its current git hash, author name and email, committer time. Crashes for an id with no stored commit (one minted in this program) |
426| `meta i` | `{ hash, author, email, time }` for the visible commit with change id `i`: its current git hash, author name and email, committer time. Crashes for an id with no stored commit (one minted in this program) |
611−| `validate repo` | `repo` unchanged if persisting it would succeed; otherwise the crash persistence would raise (§7.5 steps 1–3, the root invariant of 4, and 6). A changed repository also checks the backend's author identity (§7.9). Persistence runs the same checks |
427+| `validate repo` | `repo` unchanged if persisting it would succeed; otherwise the crash persistence would raise (§7.5 steps 1–3 and 6). Persistence runs the same checks |
612
428
613### 4.10 Ids: existing and new
429### 4.10 Ids: existing and new
614
430
@@ -636,12 +452,10 @@ renders a minted id like any other.
636
452
637### 4.11 Label literals
453### 4.11 Label literals
638
454
639−`%main` denotes `labelled "main"`, a `Revset`, as if written out where it
455+`%main` denotes `labelled "main"`, a `Revset`. It is a function, evaluated
640−stands: in the scope of a `config.j` binder named `labelled` (§4.2) it applies
641−the binder, and it is a reference to it for evaluation order (§4.1). It is a
642−function, evaluated when applied, so it never fails at parse time: a name the
456+when applied, so it never fails at parse time: a name the remote does not have
643−remote does not have yields `[]`, and `goto %nope` crashes with "expected one
457+yields `[]`, and `goto %nope` crashes with "expected one revision, got 0" as
644−revision, got 0" as any revset would.
458+any revset would.
645
459
646### 4.12 Shapes as values, and `extract`
460### 4.12 Shapes as values, and `extract`
647
461
@@ -649,8 +463,7 @@ A type name used in an expression evaluates to a *shape*: for a `typedecl`
649whose type is a record, the set of its field names; for a primitive kind
463whose type is a record, the set of its field names; for a primitive kind
650(`Int`, `Text`, `Bool`, `Id`, `Blob`), that kind; for a `typedecl` that is an
464(`Int`, `Text`, `Bool`, `Id`, `Blob`), that kind; for a `typedecl` that is an
651alias of another type, that type's shape. A shape whose type is a function
465alias of another type, that type's shape. A shape whose type is a function
652−(`Edit`, `Revset`), a list, or undeclared is a crash when used, as is an
466+(`Edit`, `Revset`), a list, or undeclared is a crash when used.
653−alias that never reaches a type (`A = B` with `B = A`).
654
467
655`extract S v` returns every subvalue of `v` that is an `S`, as a list. It
468`extract S v` returns every subvalue of `v` that is an `S`, as a list. It
656walks lists in order and records in ascending field-name order, descending
469walks lists in order and records in ascending field-name order, descending
@@ -675,32 +488,11 @@ its `root`. Where order matters, `commits (top repo)` is preorder.
675Every application of a function that has a signature — a builtin or a
488Every application of a function that has a signature — a builtin or a
676definition with a signature — is checked against it: each argument as it is
489definition with a signature — is checked against it: each argument as it is
677supplied, against the corresponding parameter type, and the result once the
490supplied, against the corresponding parameter type, and the result once the
678−function's own application completes (its body has run or the builtin has
679−all its arguments), against the result type.
491+application is complete (yields a non-function), against the result type.
680Lambdas without a signature and the command-line expression are unchecked
492Lambdas without a signature and the command-line expression are unchecked
681−except through the functions they call. A signature covers all the arguments
493+except through the functions they call. A signature on a definition that is
682−it lists, however the definition's value is curried: `describe : Text -> Edit`
683−is `\m -> mapRoot …`, and the `Repo` that `describe m` is then applied to is
684−checked as its second argument, and the result against `Repo`; with
685−`addT : Int -> Int -> Text` and `addT = (+) . (\x -> x + 1)`, the `5` of
686−`addT 1 5` is checked as its second argument, and the `7` against `Text`. A
687−signature only adds checks and never changes when a body is evaluated
688−(§4.1): `describe m` evaluates `mapRoot …` at once, so an `or` around it
689−catches a crash there (§4.6); the value must be a function, since the
690−signature lists a further argument, and shows as `describe m` (§5.2);
691−`addT 1` likewise shows as `addT 1`, not as the `(+) 2` it holds. Arguments
692−are numbered as the signature lists them, even when the definition's value is
693−a partial application or a composition holding arguments of its own (`tree 5`
694−is a violation at argument 1); when the value is an `or` of functions (§4.6),
695−the check is around the lifted function, so a result that violates the
696−signature is a crash, not a reason to try the other side. A signature on
697−a definition that is not a function (`user`, `conflicts`) is checked once,
494+not a function (`user`, `conflicts`) is checked once, against the value, when
698−against the value, when the definition is evaluated at load.
495+the definition is evaluated at load.
699−
700−Infix `&&` and `||` must be declared like other builtins. Their contracts
701−check each operand that is evaluated and the result, including a result
702−decided by the left operand alone. A skipped right operand is neither
703−evaluated nor checked.
704
496
705The check is one level deep and never inspects inside a list or a record's
497The check is one level deep and never inspects inside a list or a record's
706field values:
498field values:
@@ -712,7 +504,7 @@ field values:
712| `{ a : T, … }`, or a shape name | the value is a record with exactly those field names |
504| `{ a : T, … }`, or a shape name | the value is a record with exactly those field names |
713| `A -> B` | the value is a function |
505| `A -> B` | the value is a function |
714| a type variable | anything |
506| a type variable | anything |
715−| an alias (`Edit`, `Revset`, `Snapshot`) | the type it stands for; an alias of a function type is unfolded so that further arguments and the final result are checked against it (`at : Revset -> Edit -> Edit` checks three arguments and a `Repo` result); an alias the unfolding has already unfolded is not unfolded again, so `Stream = Int -> Stream` checks one argument and a function result |
507+| an alias (`Edit`, `Revset`, `Snapshot`) | the type it stands for; an alias of a function type is unfolded so that further arguments and the final result are checked against it (`at : Revset -> Edit -> Edit` checks three arguments and a `Repo` result) |
716
508
717Field values and list elements are not checked because that would make every
509Field values and list elements are not checked because that would make every
718call cost the size of the value and would force lazily backed snapshots and
510call cost the size of the value and would force lazily backed snapshots and
@@ -745,9 +537,7 @@ indented under their key or bullet. Colour follows §7.11's rules and is used
745only when stdout is a terminal; nothing is conveyed by colour alone.
537only when stdout is a terminal; nothing is conveyed by colour alone.
746
538
747Shapes are recognised by their exact field set, as in §4.12: `Commit`, `Entry`,
539Shapes are recognised by their exact field set, as in §4.12: `Commit`, `Entry`,
748−`Subtree`, `Repo`, `Frame`, `Change`, checked in that order using their
749−configured definitions. Additional type aliases do not displace these
750−display forms. Any other record is generic.
540+`Subtree`, `Repo`, `Frame`, `Change`. Any other record is generic.
751
541
752| kind | line form | block form |
542| kind | line form | block form |
753|---|---|---|
543|---|---|---|
@@ -756,7 +546,7 @@ display forms. Any other record is generic.
756| `Text` | raw, first line, `…` if truncated | raw, all of it, trailing newline added if absent |
546| `Text` | raw, first line, `…` if truncated | raw, all of it, trailing newline added if absent |
757| `Id` | shortest unique prefix (bold), remainder (dim) | same |
547| `Id` | shortest unique prefix (bold), remainder (dim) | same |
758| `Blob` | `‹214 B›`, `‹✖ 3.1 KB›` if unresolved | its content, raw (markers if unresolved) |
548| `Blob` | `‹214 B›`, `‹✖ 3.1 KB›` if unresolved | its content, raw (markers if unresolved) |
759−| function | name, lambda source, or `f arg …`; first line, `…` if truncated | same, all of it |
549+| function | name, lambda source, or `f arg …` | same |
760| `Shape` | its type name | same |
550| `Shape` | its type name | same |
761| `Commit` | `glyph id message labels` — a `tree` line without rails | that line, then `author · age · n files`, then one changed path per line with `+ ~ − ✖` marks |
551| `Commit` | `glyph id message labels` — a `tree` line without rails | that line, then `author · age · n files`, then one changed path per line with `+ ~ − ✖` marks |
762| `Entry` | `path size`, `✖` prefix if unresolved | same |
552| `Entry` | `path size`, `✖` prefix if unresolved | same |
@@ -772,35 +562,10 @@ display forms. Any other record is generic.
772| list of other scalars | `a, b, c` | same, wrapped at the terminal width |
562| list of other scalars | `a, b, c` | same, wrapped at the terminal width |
773| any other list | `n items` | one block per item, separated by a blank line |
563| any other list | `n items` | one block per item, separated by a blank line |
774
564
775−Column tables use copyable shortest unique `@` prefixes for Id cells and
776−show the actual contents of Text-list cells (such as bookmark names), not
777−item counts. Empty Text-list cells are blank. On a terminal, columns are
778−fitted to the available width; if minimum column widths cannot fit, rows
779−are shown vertically with wrapped values. Piped tables retain untruncated
780−cell contents. These presentation choices do not change `show` or values.
781−
782−The reference status record includes `unrecorded : [Path]` alongside
783−`changed : [Path]`. The display labels `changed` as `in commit` to distinguish
784−changes against the commit's parent from file edits since the last recording.
785−Its empty fields read `No changes`, `No conflicts`, `No labels`,
786−`No description`, and `All edits recorded` for empty `unrecorded`.
787−Nonempty paths and labels remain visible. Presentation does not rename the
788−underlying `changed` field. Older five-field status records remain displayable.
789−
790An empty list of any kind prints `none` (dim) in block form and `0 …` in
565An empty list of any kind prints `none` (dim) in block form and `0 …` in
791line form. Author and age in a `Commit` block are read from jj's commit
566line form. Author and age in a `Commit` block are read from jj's commit
792metadata by id;
567metadata by id;
793−they are not fields of the value. Nor is where it stands, which is read by id
794−from `r`, the repository the expression was given (§1.2 step 4, after the
795−snapshot): a block's paths are those it changes against its parent's files
796−there, and its glyph, in either form, is the one `tree` draws for it there,
797−with `⊗` and `◌` judged by the value's own files. What a commit with no
798−parent there (the root, or one the expression made) changes is not known:
799−its block lists its files unmarked (but for `✖` on an unresolved one), and
800−its glyph claims no `◌`. One the expression made has no metadata either
801−(§7.11), so its block has no `author · age · n files` line. One the
802−expression moved is still compared with its parent there. A `Repo`'s
803−line draws its focus as the focus. A list of `[Text]` prints as paths because
568+they are not fields of the value. A list of `[Text]` prints as paths because
804that is what such a list is in every base function; `show` is available
569that is what such a list is in every base function; `show` is available
805when the literal is wanted. Alignment in tables and columns uses display
570when the literal is wanted. Alignment in tables and columns uses display
806width (East Asian wide characters count two cells, combining marks zero), not
571width (East Asian wide characters count two cells, combining marks zero), not
@@ -810,13 +575,12 @@ Examples:
810
575
811```
576```
812$ j status
577$ j status
813−in commit src/lexer.rs
578+changed src/lexer.rs
814− src/parser.rs
579+ src/parser.rs
815−conflicts src/lexer.rs
580+conflicts src/lexer.rs
816−id wqzt·kpqxmnrvyxskptlmzzzzabcd
581+id wqzt·kpqxmnrvyxskptlmzzzzabcd
817−labels feature
582+labels feature
818−message wip
583+message wip
819−unrecorded All edits recorded
820
584
821$ j conflicts
585$ j conflicts
822⊗ wqzt wip feature
586⊗ wqzt wip feature
@@ -859,18 +623,10 @@ j: crash: expected one revision, got 0
859
623
860### 5.2 `show`
624### 5.2 `show`
861
625
862−When it succeeds, `show v` renders `v` as an expression in the language, such
626+`show v` renders `v` as an expression in the language, such that for every
863−that for every value except closures, unresolved blobs, and values nested past the parser's
627+value except closures and unresolved blobs, `show v` parses and evaluates to a
864−bound (§3.4: a list 40,000 deep, a record about 20,000), `show v` parses and
865−evaluates to a value equal to `v` under the same `config.j`. It never uses
628+value equal to `v` under the same `config.j`. It never uses colour and never
866−colour and never truncates. A value holding a part the store cannot supply (a
629+truncates.
867−`files` list or a file's content, §7.2) crashes `show` as it crashes any other
868−use: `show` writes no stand-in for the part.
869−
870−Binary blobs and non-regular file kinds require the declaration
871−`blobHex : Text -> Text -> Blob` in `config.j`; if it is missing, `show`
872−crashes with instructions to add it. Existing custom configs are not
873−modified automatically.
874
630
875| kind | rendering |
631| kind | rendering |
876|---|---|
632|---|---|
@@ -880,33 +636,18 @@ modified automatically.
880| list | `[v1 v2 …]` |
636| list | `[v1 v2 …]` |
881| record | `{ a = v, b = w }`, fields in ascending lexicographic order |
637| record | `{ a = v, b = w }`, fields in ascending lexicographic order |
882| `Id` | `@` followed by the full id, e.g. `@wqztkpqxmnrvyxskptlmzzzzabcd` |
638| `Id` | `@` followed by the full id, e.g. `@wqztkpqxmnrvyxskptlmzzzzabcd` |
883−| resolved regular `Blob` with valid UTF-8 content | `blob "…"` |
639+| resolved `Blob` | `blob "…"` |
884−| other resolved `Blob` | `blobHex "kind" "…"`, preserving file kind and bytes, with lowercase hexadecimal content |
885| unresolved `Blob` | `{- unresolved -} blob "…"` with the conflict-marker rendering |
640| unresolved `Blob` | `{- unresolved -} blob "…"` with the conflict-marker rendering |
886| builtin | its declared name, operators parenthesised: `map`, `(++)` |
641| builtin | its declared name, operators parenthesised: `map`, `(++)` |
887−| selector | the selector itself: `.id` |
888−| top-level definition with a signature whose value is a function | its name |
642+| top-level definition | its name |
889| revset from a label literal | `%name` |
643| revset from a label literal | `%name` |
890| shape | its type name: `Commit`, `Id` |
644| shape | its type name: `Commit`, `Id` |
891| lambda closure | its source text, exactly as written |
645| lambda closure | its source text, exactly as written |
892| partial application | the function's rendering followed by the arguments' renderings, each parenthesised if not atomic: `describe "wip"`, `at (parents) (describe "x")` |
646| partial application | the function's rendering followed by the arguments' renderings, each parenthesised if not atomic: `describe "wip"`, `at (parents) (describe "x")` |
893
647
894−A lambda's source text runs from its `\` to the last token of its body,
895−comments within it included, and a section's is the section itself, which
896−unlike a lambda is an atom: `map (+ 1)`, `(\x y -> x + y) 1`.
897−
898−A top-level definition with a signature whose value is a function renders as
899−its name whatever that function is built from, and its partial application
900−as its name followed by the arguments it was given, never by those its value
901−holds: `prev`, not `prev (parents)` (with `prev = goto parents`), `tree`, not
902−`treeWith ({ … })`, and `addT 1` (§4.13). Any other definition renders as its
903−value: `user` as its record, and without signatures `inc = \x -> x + 1` as
904−`\x -> x + 1` and `incAll = map inc` as `map (\x -> x + 1)`.
905−
906−Line breaking: one line if it fits in 80 columns (display width, as in §5.1);
648+Line breaking: one line if it fits in 80 columns; otherwise the outermost
907−otherwise the outermost list or record breaks one element per line, two-space
649+list or record breaks one element per line, two-space indented, with leading
908−indented, with leading commas for records and nothing between list elements,
650+commas for records and nothing between list elements, recursively.
909−recursively.
910
651
911Since `show` returns a `Text` and a top-level `Text` displays raw,
652Since `show` returns a `Text` and a top-level `Text` displays raw,
912`j show . focus` prints the literal of the focused commit, and
653`j show . focus` prints the literal of the focused commit, and
@@ -919,48 +660,14 @@ Since `show` returns a `Text` and a top-level `Text` displays raw,
919### 6.1 Location and loading
660### 6.1 Location and loading
920
661
921`j` reads `$XDG_CONFIG_HOME/j/config.j`, defaulting to `~/.config/j/config.j`.
662`j` reads `$XDG_CONFIG_HOME/j/config.j`, defaulting to `~/.config/j/config.j`.
663+It is read by every invocation except `remote`, `fetch`, `undo`, `redo`, and
922−Expression evaluation and `push` load the configuration; `init` and
664+`ops`: expression evaluation and `push` need its definitions, `init` and
923`clone` need `user`. When there is no repository yet (`init`, `clone`), only
665`clone` need `user`. When there is no repository yet (`init`, `clone`), only
924`user` is evaluated; `Id` literals are not resolved and no other definition
666`user` is evaluated; `Id` literals are not resolved and no other definition
925−is evaluated. `remote`, `fetch`, `undo`, `redo`, and `ops` do not load configuration.
667+is evaluated. If it does not exist, `j` exits 3 with
926−When a command needs configuration and none exists, the binary writes its
668+`j: no config at <path>; the reference config.j is installed at <share-path>`.
927−embedded reference `config.j` (§9) to that path as an editable starting point,
669+The binary is distributed with the reference `config.j` (§9) at
928−readable and writable by its owner, and reports its path. An existing file is
670+`<prefix>/share/j/config.j`; `j` never writes to the config path itself.
929−never overwritten, including when it cannot be read or contains invalid
930−UTF-8; such errors exit 3.
931−Configuration diagnostics identify the actual file; parse errors include the
932−line number and a bounded source excerpt, with a pointer to `j help config`.
933−Excerpts escape terminal controls and are capped at 160 display columns and
934−characters, with an ellipsis when truncated. Expression parse errors, including
935−push expressions, likewise identify their source line. Runtime diagnostics may
936−add typo suggestions from the actual vocabulary, quoting examples for Text
937−arguments, or missing-argument guidance derived from the command signature;
938−they never retry an expression or change its meaning or exit code.
939−An existing user configuration is not updated when the binary changes. The
940−reference file is also distributed at `<prefix>/share/j/config.j`.
941−
942−Help and command discovery are read-only exceptions (§1.1.1): general help and
943−reserved-command help need no config, while the catalog and help for config
944−names read it without evaluation. If it is missing they use the embedded
945−reference in memory without writing a file. The empty-invocation guide does
946−not read or write configuration either.
947−
948−### 6.1.1 Explicit setup
949−
950−`setup` creates the full embedded configuration at the configured path without
951−opening a repository. With no arguments, it creates an editable default and
952−reports the path and instructions to set `user`. With `NAME EMAIL`, it checks
953−the identity rules of §6.2 and writes those values into the complete reference
954−configuration, serialized as language text literals. Names containing spaces
955−are supplied as a separately quoted shell argument.
956−
957−Setup never overwrites an existing file. Without identity arguments, an
958−existing file reports its effective identity when valid, or guidance to
959−edit it otherwise. With identity arguments, an
960−existing file is a usage error (exit 2), with instructions to edit manually.
961−Argument counts other than zero or two are usage errors. Creating the file
962−also creates missing parent directories; exclusive creation preserves a file
963−created concurrently by another invocation. No repository state is changed.
964
671
965### 6.2 Validation
672### 6.2 Validation
966
673
@@ -985,13 +692,6 @@ not declared is simply unbound, and using it is a runtime crash.
985
692
986Definitions may reference each other in any order.
693Definitions may reference each other in any order.
987
694
988−Before authoring commits (`init`, `clone`, or an expression that persists a
989−change), the effective identity must also have nonblank name/email and must
990−not retain the shipped `Your Name` or `you@example.com` placeholders. Such
991−identity failures are configuration errors (exit 3) and do not persist the
992−edit. Inspection and unchanged expressions remain usable with placeholder
993−identity, so users can inspect and repair existing repositories.
994−
995### 6.3 Definitions the interpreter reads
695### 6.3 Definitions the interpreter reads
996
696
997Four names in `config.j` are read by the interpreter itself, not only by
697Four names in `config.j` are read by the interpreter itself, not only by
@@ -1020,10 +720,6 @@ but nothing here requires the `jj` binary to be installed.
1020Walk up from the current directory to the first directory containing `.jj`.
720Walk up from the current directory to the first directory containing `.jj`.
1021Only the default workspace is supported; if the working directory belongs to
721Only the default workspace is supported; if the working directory belongs to
1022another workspace, exit 2.
722another workspace, exit 2.
1023−The working copy must also include the whole repository: sparse patterns
1024−must contain the root path. Otherwise exit 2 without changing the patterns,
1025−files, or operations, with instructions to run
1026−`jj sparse set --clear --add .` before using `j`.
1027
723
1028### 7.2 Building the `Repo` value
724### 7.2 Building the `Repo` value
1029
725
@@ -1051,16 +747,11 @@ a value as follows.
1051 `fetch` found the remote's `main` pointing at it — the snapshot cannot go
747 `fetch` found the remote's `main` pointing at it — the snapshot cannot go
1052 into it; the focus is instead a new empty child of it holding the snapshot,
748 into it; the focus is instead a new empty child of it holding the snapshot,
1053 as `abandon` would leave it, and persisting records that child. Nothing
749 as `abandon` would leave it, and persisting records that child. Nothing
1054− else is written by loading. The value as loaded (§1.2 step 8) has the same
750+ else is written by loading.
1055− child holding the commit's own files, so `j id` records the child exactly
1056− when the working directory differs from that commit.
1057
751
1058The record is the *semantics*. An implementation may back `files`, `children`,
752The record is the *semantics*. An implementation may back `files`, `children`,
1059and `context` with lazily materialised structures, provided every observation
753and `context` with lazily materialised structures, provided every observation
1060−an expression can make agrees with the eager value. If materialising one of
754+an expression can make agrees with the eager value.
1061−them, or a file's content, fails (an object missing from the store), every
1062−later use of it fails the same way: a crash caught by `or` (§4.7) never leaves
1063−some other value in its place.
1064
755
1065### 7.3 Files, blobs, and `replay`
756### 7.3 Files, blobs, and `replay`
1066
757
@@ -1068,70 +759,21 @@ some other value in its place.
1068 the list of path components; components must be valid UTF-8, and a
759 the list of path components; components must be valid UTF-8, and a
1069 repository with a path that is not exits 2. `content` is a `Blob` carrying the file's
760 repository with a path that is not exits 2. `content` is a `Blob` carrying the file's
1070 content and type (regular, executable, symlink) and, for conflicted paths,
761 content and type (regular, executable, symlink) and, for conflicted paths,
1071− jj's conflict value for that path, less each pair of sides that cancel (a
762+ jj's conflict value for that path.
1072− side added and one removed that are the same), as jj drops them to write
1073− a file's conflict out. jj keeps such pairs in the conflicts of the trees
1074− it merges; dropped here, a conflict a commit inherits unchanged reads as
1075− its parent's. A conflict whose sides left would all be directories keeps
1076− every side, as jj keeps it as one conflict at the path rather than one
1077− between directories. The entries' order carries no meaning,
1078− as a tree has none: persistence (§1.2 step 8, §7.5), the laws of §8 and
1079− the tree's empty mark compare snapshots path by path, though `==` compares
1080− them as lists (§4.5).
1081- A path that is deleted and resolved has no entry. A path that is in a
763- A path that is deleted and resolved has no entry. A path that is in a
1082− deletion conflict has an entry whose blob is unresolved. Each side of a
764+ deletion conflict has an entry whose blob is unresolved.
1083− conflict is either absent or content with its own file type, so a side
1084− that deleted the path is not an empty file. In a conflict between a file
1085− and a directory, which jj keeps as one path, a directory side reads as
1086− empty content but is kept whole, and is written back as that directory.
1087- `replay onto { from, to }` is jj's tree merge: for each path present in
765- `replay onto { from, to }` is jj's tree merge: for each path present in
1088 any of the three snapshots, the three-way merge of that path's values with
766 any of the three snapshots, the three-way merge of that path's values with
1089 `from` as the common ancestor, replaying the change from `from` to `to` on
767 `from` as the common ancestor, replaying the change from `from` to `to` on
1090 top of `onto`. Conflicts are kept as unresolved blobs, and resolved where
768 top of `onto`. Conflicts are kept as unresolved blobs, and resolved where
1091 jj's merge would resolve them (identical changes, changes from the current
769 jj's merge would resolve them (identical changes, changes from the current
1092− content, non-overlapping line-level hunks). A path unresolved in any of the
770+ content, non-overlapping line-level hunks). It never crashes on content. It
1093− three takes part with all its sides, as when jj rebases a conflicted commit,
1094− so sides that cancel drop out and a replay can resolve a conflict as well
1095− as keep one. A conflict it keeps has no pair of sides that cancel
1096− (above), so in a stack replayed commit by commit, a conflict each commit
1097− inherits is its parent's, whatever conflicts the commits below added. It
1098− never crashes on content, except where a file meets a
1099− directory (below). It crashes if any of the three is not a well-formed
771+ crashes if any of the three is not a well-formed snapshot (a list of `Entry`
1100− snapshot (a list of `Entry` records with unique paths, none of them the
772+ records with unique paths).
1101− root `./`, and none both a file and a directory as `./a` beside `./a/b`
1102− would be).
1103−- A path that is a file in one of the three snapshots and a directory in
1104− another is merged as one value, as jj does, the directory with everything
1105− below it. When at most one of `onto` and `to` changed it from `from`, that
1106− value is taken whole. When both did and none of the three holds a
1107− conflict at or below the path, the result is their value taken whole if
1108− they made the same change, and otherwise a conflict at the path, its
1109− directory sides kept whole. A conflict one of the three holds there takes
1110− part with all its sides, as above, so the result is a conflict at the path
1111− unless sides cancel, even where both made the same change: a child that
1112− resolves a conflict at `./a` into `./a/b`, replayed with `from` that
1113− conflict onto the same resolution, keeps a conflict at `./a`. `replay`
1114− crashes, naming the path, when its result would be a conflict at the path
1115− with a file on one side and on another a directory that `onto` or `to`
1116− holds, as replaying the addition of `./a/b` onto a file `./a` would: one
1117− entry cannot list the directory's entries. It does not crash where one of
1118− the three already holds a conflict at that path (an unresolved blob there,
1119− not one below it): the sides came in with that conflict and are carried
1120− like any others, so a child that resolves such a conflict can be squashed
1121− into it. The in-memory backend of §10 merges such a path file by file,
1122− like any other, keeps no directory as a side, and crashes when its result
1123− would hold both `./a` and `./a/b`. So here, as where jj merges lines, the
1124− two backends may differ: it resolves some merges that jj keeps as a
1125− conflict or refuses (a change from a file `./a` to `./a/b` replayed onto a
1126− snapshot without `./a` gives `./a/b`, and so does one from a conflict at
1127− `./a` to `./a/b` replayed onto `./a/b`; the deletion of `./a/z` replayed
1128− onto a snapshot where `./a` is a file gives that file), and it may crash
1129− where jj carries a conflict that one of the three held at `./a`.
1130−- `unresolved`, `blob`, `blobHex`, and `text` are as in §4.9. `blob` produces
773+- `unresolved`, `blob`, and `text` are as in §4.9. `blob` produces a regular
1131− a regular file from text. `blobHex` constructs a regular file, executable,
774+ file. File type (executable, symlink) travels with a `Blob` but cannot be
1132− or symlink from its kind and hexadecimal bytes, so `show` can preserve both
1133− content and file type. `text` of a symlink is its target; `text` of a
775+ changed in the language. `text` of a symlink is its target; `text` of a
1134− non-UTF-8 blob crashes. Persisting a symlink with a non-UTF-8 target crashes.
776+ non-UTF-8 blob crashes.
1135
777
1136### 7.4 Working-directory invariant
778### 7.4 Working-directory invariant
1137
779
@@ -1141,100 +783,18 @@ directory.
1141- **Before evaluation**, the working directory is snapshotted into the
783- **Before evaluation**, the working directory is snapshotted into the
1142 working-copy commit, and that is the `files` of the focus in the `Repo`
784 working-copy commit, and that is the `files` of the focus in the `Repo`
1143 value. The snapshot tracks every file not matched by `.gitignore` files,
785 value. The snapshot tracks every file not matched by `.gitignore` files,
1144− and each file it tracked before that a `.gitignore` has come to match
1145− (deleted once it is gone, even where a file has replaced a directory
1146− above it), with no size limit, symlinks as symlinks, and nothing inside a
786+ with no size limit, symlinks as symlinks; these rules are fixed, since `j`
1147− nested repository (a directory below the root holding `.git` or `.jj`);
1148− these rules are fixed, since `j` reads no jj configuration. If the snapshot
787+ reads no jj configuration. If the snapshot changed the commit, that change
1149− changed the commit, that change is part of the operation persisted
1150− afterwards, or discarded with the rest if nothing is persisted; a change
788+ is part of the operation persisted afterwards, or discarded with the rest if
1151− that leaves the commit with its parent's files, as where the directory
1152− reverts what the commit changed, gives it the parent's tree, as step 4
1153− of §7.5 writes such a commit, so that jj takes it for empty. A crashed
1154− or printing program leaves the working copy and the repository exactly as
789+ nothing is persisted. A crashed or printing program leaves the working copy
1155− before (but see §7.5 step 7); `j id` is the way to record the working
790+ and the repository exactly as before; `j id` is the way to record the
1156− directory and nothing else (§1.2).
791+ working directory and nothing else (§1.2).
1157−- **A stale working copy.** The working copy is stale when the files last
1158− checked out are not the working-copy commit's: jj changed the commit
1159− without updating the working directory (a command run with
1160− `--ignore-working-copy`, or one in another workspace that rewrites this
1161− workspace's commit), `fetch` rebased it (§7.6), or a checkout failed or
1162− was cut short after its operation was recorded (§7.5 step 7). The
1163− working copy's record of the files last checked out names the operation
1164− it was saved at, and one saved at the head operation is current whatever
1165− files it records, as jj takes it: where the record was lost, and jj made
1166− it again recording no file, the snapshot records the directory as it
1167− is. Over a stale working copy, the directory started from the files last
1168− checked out, and what has changed in it since is a change to those,
1169− which the snapshot replays onto the commit: a three-way merge with the
1170− files last checked out as the base, in which a path that the directory
1171− and the commit both changed, differently, is a conflict (an edit to a
1172− file the commit deletes, say), and one where the directory holds what
1173− the commit does is the commit's (what a checkout cut short had written).
1174− A path that both changed where that conflict would set a directory
1175− against a file, a symlink or nothing (a file the commit changes that the
1176− user replaced by a directory, say), which `replay` refuses as one entry
1177− cannot list the directory's entries (§7.3), takes what the working
1178− directory holds there instead, as over a working copy that is not stale:
1179− the commit's change there is left in the operation before. A conflict of
1180− that kind that the commit itself has (one made by a `fetch` that rebased
1181− it, say) stays where the directory has not changed the path, which is
1182− the commit's like any other. Either way, where the record does not hold
1183− one of the commit's conflicts, a file holding what a checkout writes for
1184− it is read back as that conflict: exactly its markers, or a description
1185− of the same sides, whatever their labels and less any side it both
1186− removes and adds. With no change there is nothing to snapshot, and a
1187− program that persists writes the focus over the files last checked out
1188− as usual. A stale working copy refuses no program.
1189- **After persistence**, the focus of the result is checked out: its files are
792- **After persistence**, the focus of the result is checked out: its files are
1190 written to the working directory, unresolved blobs materialised with jj's
793 written to the working directory, unresolved blobs materialised with jj's
1191 conflict markers, and the workspace's working-copy commit set to the focus.
794 conflict markers, and the workspace's working-copy commit set to the focus.
1192− A checkout writes nothing over what stands where the focus has a file (or
1193− a symlink, or a conflict) and the directory has no tracked one. It first
1194− removes each tree of empty directories there, which no snapshot sees (a
1195− tool may leave one, and a failed checkout does, for a path it did not
1196− reach), and moves an untracked file holding just what it writes there
1197− (as a checkout cut short leaves at an ignored path) aside into `.jj`,
1198− which no snapshot reads, looking through no symlink, so nothing outside
1199− the working directory is touched. Once the checkout returns, each file
1200− moved aside is put back where nothing stands, as where a failed checkout
1201− did not get to write, and, where the checkout failed (a full disk), over
1202− a file holding a start of it, less than all of it (an empty file, for a
1203− symlink), as a write cut short leaves, completing the write; the rest
1204− are removed, each holding just what the focus has there, which the
1205− repository keeps, and what stands there, what the checkout wrote or
1206− something put there since, stays. What a run killed meanwhile leaves in
1207− `.jj`, the next run puts back as after a failed checkout, before it
1208− looks at the working directory. A file that cannot be moved stays, and the
1209− checkout, which skips its path, fails, naming it and why (§7.5 step 7).
1210− Anything else untracked there, or in place of a directory above the path
1211− (an ignored file, a symlink, a directory holding one), crashes the program
1212− before its operation is recorded, as git refuses to write over an
1213− untracked file, naming each such thing (the first ten, and how many more
1214− there are), a directory by its own path and an untracked file in it:
1215− each is to be moved aside.
1216− A later snapshot reads such a file back as the conflict its markers
1217− describe, edits included, while they parse at the length they were written
1218− with (longer than 7 when a side holds a marker-like line); once they do
1219− not, the file is resolved to its text. A conflict with a side that is no
1220− file (a directory, a symlink) has no markers: it is written as jj's
1221− description of its sides, and read back as that conflict while the file
1222− is left as written (the working copy records each file's size and
1223− modification time) or describes the same sides, whatever their labels
1224− and less any side it both removes and adds (a snapshot may drop such a
1225− pair from the conflict without writing the file again); a file written
1226− with anything else is resolved to its text.
1227
795
1228Reserved commands never snapshot.
796Reserved commands never snapshot.
1229
797
1230−The `unrecorded : Repo -> [Path]` builtin compares the invocation's loaded
1231−focus files with its snapshotted focus files, using the same path/content
1232−comparison as `changed`. The result describes the original working-copy
1233−edits, independently of changes an expression makes to its `Repo` argument.
1234−It returns `[]` when either snapshot context is absent, including reserved
1235−push evaluation. Read-only status and preview calls do not record these paths;
1236−`save = id` persists their snapshot through the normal expression path.
1237−
1238### 7.5 Persistence
798### 7.5 Persistence
1239
799
1240Given the loaded value `old` and the result `new`, one jj operation is
800Given the loaded value `old` and the result `new`, one jj operation is
@@ -1242,27 +802,8 @@ recorded, described by the expression text (truncated to 200 characters).
1242Within it:
802Within it:
1243
803
12441. **Validate shape.** `new` has the shape of a `Repo`; every `id` occurs
8041. **Validate shape.** `new` has the shape of a `Repo`; every `id` occurs
1245− exactly once; every commit's `message` is a `Text`; every snapshot entry
805+ exactly once; every snapshot has unique paths. Violations crash with a
1246− has a `path` list of texts and a `content` that is a `Blob`; every snapshot
806+ message naming the rule.
1247− has unique paths, none of them the root `./` and none both a file and a
1248− directory (§7.3); every path
1249− component of the focus, of every other commit that is not in `old`, and
1250− of every path any other commit holds that it did not hold in `old`
1251− (§7.3), is a name a checkout can create: not empty, `.`, `..`, `.git` or
1252− `.jj`, without `/`
1253− or NUL, and a name the workspace's filesystem can hold. A name the working
1254− directory holds (§7.4) is one. A name longer than 255 bytes, the limit of
1255− ext4, is tried on the filesystem, since exFAT, NTFS and APFS count 255
1256− UTF-16 units or characters instead; where a filesystem's limit is lower,
1257− or a whole path outgrows the system's limit, the checkout of step 7
1258− refuses it instead. Where the working directory's filesystem folds case
1259− (`.JJ` names its `.jj`), `.git` and `.jj` are refused in any letter case.
1260− (Any other path is neither checked out nor written with a name jj has not
1261− already stored, and a fetched branch can hold any name git can, even one
1262− above the focus that a snapshot rebases.) Violations crash with a message
1263− naming the rule. Files as loaded from a stored tree (§7.2) are such a
1264− snapshot already, as a jj tree holds each name once and each path as a
1265− file or as a directory, and are not checked again.
12662. **Validate labels.** The set of `(id, label)` pairs in `new` equals the set
8072. **Validate labels.** The set of `(id, label)` pairs in `new` equals the set
1267 in `old`. Labels are the remote's names (§7.6); a script cannot add, move,
808 in `old`. Labels are the remote's names (§7.6); a script cannot add, move,
1268 or remove one. In particular a commit carrying a label cannot be abandoned.
809 or remove one. In particular a commit carrying a label cannot be abandoned.
@@ -1271,9 +812,7 @@ Within it:
1271 and all of its ancestors. Every id in it must be present in `new` with the
812 and all of its ancestors. Every id in it must be present in `new` with the
1272 same parent id, files, and message. Violations crash naming the commit.
813 same parent id, files, and message. Violations crash naming the commit.
1273 (Ancestors of merges are included because rewriting an ancestor rewrites
814 (Ancestors of merges are included because rewriting an ancestor rewrites
1274− the merge, whose second parent the model cannot represent.) They are the
815+ the merge, whose second parent the model cannot represent.)
1275− merge's jj ancestors through every parent, not only the first, so a
1276− commit reached only through a second parent is immutable too.
12774. **Walk `new` top-down.** For each commit `c` (root first):
8164. **Walk `new` top-down.** For each commit `c` (root first):
1278 - If `c.id` is the root commit's change id, it must be the top of the tree
817 - If `c.id` is the root commit's change id, it must be the top of the tree
1279 and must be unchanged; otherwise crash.
818 and must be unchanged; otherwise crash.
@@ -1295,62 +834,12 @@ Within it:
1295 `new.root.id`. (`abandon` and `squash` end with `new` for this reason, so
834 `new.root.id`. (`abandon` and `squash` end with `new` for this reason, so
1296 they are safe on a commit directly above `main`; navigation onto `main`
835 they are safe on a commit directly above `main`; navigation onto `main`
1297 is written `new . goto trunk`.)
836 is written `new . goto trunk`.)
1298−7. Record the operation, then perform the checkout of §7.4, as jj does. A
837+7. Commit the operation, then perform the checkout of §7.4.
1299− signal to stop that comes meanwhile waits until the checkout and its
1300− record are saved (§1.3). An operation that cannot be recorded, a
1301− checkout that would write over something untracked (§7.4), and one that
1302− would write over a change made to the working directory while the
1303− program ran (a file saved, created or removed after the run's snapshot,
1304− at a path where the focus differs from what that snapshot read), which
1305− it names, crash before the checkout, so nothing is recorded or written.
1306− A checkout that fails (a path the filesystem cannot hold, a full disk)
1307− crashes with the operation recorded and the working directory partly
1308− updated, and says so; so does one that completes but whose record of
1309− what it wrote cannot be saved. Nothing it wrote is taken back: the
1310− working copy is left stale (§7.4), recording the files the directory
1311− held when the checkout began, so the next run takes what the checkout
1312− wrote for the focus's own and what else has changed since for a change
1313− to the focus, the next program that persists writes the rest of the
1314− focus, and `j undo` goes back. A file saved meanwhile at a path after
1315− the checkout wrote there, or at a path the focus adds before the
1316− checkout got there, and a file a full disk cut short where no file was
1317− moved aside for it, are conflicts with the focus's content there, so
1318− neither is lost (at an ignored path, a file a full disk cut short is not
1319− read: the next checkout names it as in its way, and `j undo` leaves it);
1320− a file the focus changes or removes, saved while the checkout runs but
1321− before it reaches that path, is written over, as in jj. The checkout
1322− reads no file into the repository and removes none it does not replace:
1323− a file it moved aside to write again (§7.4) is put back where it did not
1324− get to, and over the start of it that it wrote where it was cut short,
1325− and where anything else stands is removed, holding just the focus's
1326− content there; an ignored file saved meanwhile where the focus adds one,
1327− before the checkout reached it, stays, holding what it held, and the
1328− checkout fails, naming it, so that no run reads it; anything inside a
1329− nested repository stays too. No uncommitted edit present at the run's
1330− snapshot is lost where the checkout writes over it, as the operation
1331− holds that snapshot (§7.4). This is the one crash that records anything.
1332
838
1333Labels are not written by persistence at all; they are derived from remote
839Labels are not written by persistence at all; they are derived from remote
1334refs, which only `fetch` and `push` change. The interpreter may run any part
840refs, which only `fetch` and `push` change. The interpreter may run any part
1335of the walk through jj-lib's rebase machinery provided the stored result
841of the walk through jj-lib's rebase machinery provided the stored result
1336−equals what the steps above produce. As in §1.2 step 8, steps 3 and 4 may
842+equals what the steps above produce.
1337−compare tree hashes rather than values: files still as loaded from a stored
1338−tree are that tree's. Step 4 writes a commit whose files are still as
1339−loaded from a stored tree, its own or another's (`new` gives the child its
1340−parent's), with that tree as jj stored it, pairs of sides that cancel and
1341−conflict labels included (§7.3), as jj's own `new` and `describe` keep a
1342−tree: one built from the files could be another tree for the same files.
1343−For the same reason a commit step 4 writes whose files are its parent's is
1344−written with the parent's tree, labels included, whatever tree its files
1345−were loaded from, as jj's own `squash` or `rebase` leaves a commit it
1346−empties: jj takes a commit for empty only where its tree is its parent's.
1347−`squash`, `rebase` or `abandon` can leave such a commit by building its
1348−files anew, or by moving it, its files unchanged, onto another tree for
1349−the same files: an empty child of a commit squashed into its parent, or
1350−an empty commit rebased onto one holding its parent's files. A persisting
1351−run then reads the files of the focus, of the commits it writes with
1352−other files, and of those whose files it compares with a tree they were
1353−not loaded from, but not those of the rest of the history.
1354
843
1355### 7.6 Labels and the remote
844### 7.6 Labels and the remote
1356
845
@@ -1362,11 +851,7 @@ language: `fetch` and `push` are the only things that change them.
1362 each remote bookmark `name` is the label `name` on the commit it points at;
851 each remote bookmark `name` is the label `name` on the commit it points at;
1363 labels for bookmarks that no longer exist on the remote are removed; commits
852 labels for bookmarks that no longer exist on the remote are removed; commits
1364 reachable from remote bookmarks become visible. Fetching never moves the
853 reachable from remote bookmarks become visible. Fetching never moves the
1365− focus or touches the working directory. It records one operation. When
854+ focus or touches the working directory. It records one operation.
1366− the import rewrites or abandons the focus or a commit below it, the focus
1367− is rebased as jj rebases descendants, and the working copy is then stale
1368− (§7.4): the next run records what has changed in the directory onto the
1369− rebased focus, and the next program that persists writes its files.
1370- **`push EXPR`** evaluates `EXPR` (§1.1); a function result is applied to
855- **`push EXPR`** evaluates `EXPR` (§1.1); a function result is applied to
1371 the recorded repository. The value must be a list whose elements are records
856 the recorded repository. The value must be a list whose elements are records
1372 of two shapes:
857 of two shapes:
@@ -1374,120 +859,54 @@ language: `fetch` and `push` are the only things that change them.
1374 creating it if absent;
859 creating it if absent;
1375 - `{ delete : Text }` — delete the bookmark of that name.
860 - `{ delete : Text }` — delete the bookmark of that name.
1376
861
1377− Before sending a valid push, inspect the working directory without saving
1378− a snapshot or scan state. If it has unrecorded edits, print a warning to
1379− stderr (also when piped or colors are disabled) explaining that push sends
1380− recorded content only, and that `j id` followed by another push includes
1381− those edits. The warning does not change the requested push's targets or
1382− prevent deliberate publication of recorded commits.
1383−
1384 Duplicates are merged; two records with the same `name` and different
862 Duplicates are merged; two records with the same `name` and different
1385 effects are a crash. It is refused (exit 1, nothing pushed) if:
863 effects are a crash. It is refused (exit 1, nothing pushed) if:
1386− - a `name` is not a valid git branch name, an `id` is not a visible commit
864+ - a `name` is not a valid git branch name, an `id` is not a visible commit,
1387− or names the synthetic root (which has no Git commit), or a delete names
1388− a bookmark the remote does not have;
865+ or a delete names a bookmark the remote does not have;
1389 - any commit that would be sent has an unresolved file or an empty
866 - any commit that would be sent has an unresolved file or an empty
1390− description (the commits that would be sent are the records' `id`s and
867+ description;
1391− their ancestors that no bookmark on `origin` reaches as of the last
1392− `fetch` or `push`; a delete sends none);
1393 - the bookmark's current target is in the immutable set (§7.5) and the
868 - the bookmark's current target is in the immutable set (§7.5) and the
1394 record would move it to a commit that does not descend from it, or delete
869 record would move it to a commit that does not descend from it, or delete
1395− it while no bookmark left on `origin` after the push reaches that target
870+ it.
1396− (those left are the bookmarks no record names, where they are as of the
871+
1397− last `fetch` or `push`, and the records' new targets);
1398− - the deletes would wait (below) and a bookmark it sets and one it deletes
1399− are a name and a name under it, such as `master` and `master/legacy`
1400− (not `master2`): git cannot hold both, so the first git push could not
1401− succeed.
1402−
1403− Targets and descent are commits as the remote has them: a target rewritten
1404− here since the last `fetch` or `push` is hidden, and not in the immutable
1405− set although the visible rewrite carries its change id and its label.
1406 Moving a bookmark whose current target is not immutable to an unrelated
872 Moving a bookmark whose current target is not immutable to an unrelated
1407− commit is allowed; that is how a rewritten stack is re-pushed. Deleting a
873+ commit is allowed; that is how a rewritten stack is re-pushed. After a
1408− bookmark on an immutable commit that another bookmark still reaches is how
1409− a merged feature's name is removed. It loses no history as long as that
1410− other bookmark is still where the last `fetch` or `push` left it: `push`
1411− does not ask the remote, so it does not notice a bookmark moved or deleted
1412− there since. The remote applies each update of a git push on its own, so
1413− when only records' new targets reach the commit, as when
1414− `rename "master" "main"` renames trunk's only bookmark, the deletes wait:
1415− the other updates are sent first, and the deletes in a second git push
1416− only once the remote has accepted every one of them. To rename trunk's
1417− only bookmark to a name under it, push a temporary label on its commit
1418− first, `push (label "tmp" (labelled "master"))`: with it on `origin`
1419− nothing waits, and `rename "master" "master/legacy"` goes as one git
1420− push; then `push (unlabel "tmp")`. The refusal gives these steps with the
1421− first of `tmp`, `tmp-1`, `tmp-2`, … that no bookmark, on `origin` or in
1422− the repository, and no record names, and that is not a name under one of
1423− those or one they are under: following them moves none of the user's
1424− bookmarks, and the refused push does not touch the label. After a
1425 successful push the labels reflect the new positions. It records one
874 successful push the labels reflect the new positions. It records one
1426− operation, even when it sends two git pushes. An empty list sends nothing
875+ operation.
1427− to the remote, records no operation, and always reports that no updates
1428− were selected, with guidance for selecting a first bookmark. A nonempty
1429− successful push reports published, deleted, and unchanged bookmark counts
1430− on an interactive terminal, including an entirely unchanged selection.
1431− If the remote accepts some of the updates and rejects others, that
1432− operation records the accepted ones, whose labels move, and `push` exits 1
1433− naming the rejected bookmarks and any deletes it did not send.
1434
876
1435Only the remote named `origin` is supported in this version; `fetch` and
877Only the remote named `origin` is supported in this version; `fetch` and
1436−nonempty `push` exit 1 with `j: no remote origin` when it is not configured.
878+`push` exit 1 with `j: no remote origin` when it is not configured.
1437Authentication is git's: the ssh agent for ssh URLs, git credential helpers
879Authentication is git's: the ssh agent for ssh URLs, git credential helpers
1438for https, as jj-lib's git backend provides them; `j` adds no prompting.
880for https, as jj-lib's git backend provides them; `j` adds no prompting.
1439Bringing work up to date is a separate expression, e.g. `rebase trunk`.
881Bringing work up to date is a separate expression, e.g. `rebase trunk`.
1440
882
1441### 7.7 Operations, undo, redo, and the lock
883### 7.7 Operations, undo, redo, and the lock
1442
884
1443−Every persisting `j` program, each `fetch`, and each nonempty `push` records
885+Every persisting `j` program, and each of `fetch` and `push`, records exactly
1444−exactly one jj operation, described by the expression or command text. A push's
886+one jj operation, described by the expression or command text. Crashes record
1445−description includes its `push ` prefix, so an expression named `init` or
887+nothing.
1446−`clone` cannot be mistaken for the operation that created the repository.
888+
1447−Crashes record nothing, except as §7.5 step 7 says.
889+**Kinds.** `j` marks each operation it records, in the operation's metadata,
1448−
890+as *normal*, *undo* (naming the operation it undid), or *redo* (naming the
1449−**Kinds.** An operation is *normal*, *undo*, or *redo*, determined by its
891+undo it reversed). Operations recorded by other tools are treated as normal,
1450−description. New undo and redo records use jj's current markers,
892+except that an operation whose description carries jj's own undo or redo
1451−`undo: restore to operation <id>` and `redo: restore to operation <id>`.
893+marker is treated accordingly, so mixed use chains correctly where the marker
1452−The id names the exact operation whose view was restored, retaining the
1453−navigation point for later undo and redo commands.
1454−Older `j` markers (`undo: <id>` naming the discarded operation and
1455−`redo: <id>` naming the reversed undo) and jj's legacy `undo operation <id>` /
1456−`redo operation <id>` markers remain readable with their original meanings.
1457−An operation without a recognized marker is normal, so mixed use degrades
1458−to "undo the newest operation" where no marker is present.
894+is present and degrades to "undo the newest operation" where it is not.
1459−
895+
1460−For current markers, start at the head and follow restored-operation links
896+**`undo`** finds the operation to undo: starting at the head, while the
1461−while their kind matches the command: undo links for `undo`, redo links for
897+current operation is an undo, move to the operation it undid and then to that
1462−`redo`. The command then selects that operation's parent. Follow links of
898+operation's parent; while it is a redo, move past the operation it reapplied.
1463−the same kind at the selected target too, and record the final target's id
899+The first normal operation reached is undone by recording a new operation
1464−and view. Reversing direction therefore restores the opposite marker's
900+that restores its parent's view, marked as an undo of it. If none is found,
1465−parent, and either CLI can continue the other's undo/redo chain.
1466−
1467−**`undo`** restores the state before the operation reached by that walk.
1468−If no parent exists, or the operation reached is the `init` or `clone`
1469−operation (§7.8), before which there was no repository, exit 1 with
1470−`j: nothing to undo`.
901+exit 1 with `j: nothing to undo`.
1471−
902+
1472−**`redo`** requires the operation reached through redo links to be an undo;
903+**`redo`** requires the head to be an undo (possibly reached through redos);
1473−it restores the state that undo discarded. Otherwise exit 1 with
904+it records an operation restoring the view of the operation that undo
1474−`j: nothing to redo`.
905+discarded, marked as a redo. Otherwise exit 1 with `j: nothing to redo`.
1475−
906+
1476−For older `j` markers, undo follows an undo's discarded operation to its
1477−parent, or a redo's reversed undo to the operation it discarded. Redo
1478−follows a redo's reversed undo to its parent until it reaches an undo,
1479−then restores that undo's parent. This preserves pending redos when a
1480−history crosses from older markers into current ones.
1481−
1482−Both refuse, before doing anything, if the working directory holds a change
907+Both refuse, before doing anything, if the working directory differs from the
1483−the focused commit does not, as a snapshot would find it (§7.4; a stale
908+focused commit's files: `j: working copy has changes not in @; run \`j id\`
1484−working copy holds none where the directory holds what was last checked out
909+to record them or discard them`. Both check out the restored focus afterwards.
1485−or what the commit has). The refusal explains that `j id` preserves those
1486−edits in a new operation, and the next undo reverses that recording before
1487−reaching the earlier operation; users should inspect `j ops` and can recover
1488−the recording with redo. Both record their operation and then check
1489−out the restored focus, as persistence does (§7.5 step 7): a checkout that
1490−fails leaves the operation recorded, and `redo` or `undo` goes back.
1491Undoing a `push` restores the recorded labels and leaves the remote untouched.
910Undoing a `push` restores the recorded labels and leaves the remote untouched.
1492
911
1493**`ops`** prints the operation log, newest first, one line each: relative
912**`ops`** prints the operation log, newest first, one line each: relative
@@ -1500,8 +919,7 @@ operation is marked. Rendered in the binary like `tree`.
1500duration.
919duration.
1501A second `j` exits 2 with `j: another j is running in this repository`.
920A second `j` exits 2 with `j: another j is running in this repository`.
1502Display-only runs are not distinguishable in advance, so they take the lock
921Display-only runs are not distinguishable in advance, so they take the lock
1503−too; reserved commands that need no repository (`help`, `commands`, `init`,
922+too; only reserved commands that need no repository (`init`, `clone`) do not.
1504−`clone`) do not.
1505The lock prevents `j` from ever producing two commits with one change id.
923The lock prevents `j` from ever producing two commits with one change id.
1506Other tools operating concurrently are outside `j`'s control; if they produce
924Other tools operating concurrently are outside `j`'s control; if they produce
1507that state, §7.2 applies.
925that state, §7.2 applies.
@@ -1513,112 +931,49 @@ that state, §7.2 applies.
1513 initialised on top of the existing git repository and its history is
931 initialised on top of the existing git repository and its history is
1514 imported (subject to §7.2). Either way a working-copy commit is created as a
932 imported (subject to §7.2). Either way a working-copy commit is created as a
1515 child of the current head (or of the root commit for an empty repository)
933 child of the current head (or of the root commit for an empty repository)
1516− and checked out; the current head is git's `HEAD`, also when it is
1517− detached. Over an existing git repository the checkout writes no file: the
1518− working directory is taken as it is, and what it holds beyond the head
1519− becomes the working-copy commit's change. A checkout that fails leaves the
1520− operation recorded, as §7.5 step 7 says. It exits 2 if the directory is
934+ and checked out. It exits 2 if the directory is already inside a jj
1521− already inside a jj repository, or if `.git` exists but is not a directory
935+ repository, or if `.git` exists but is not a directory (a git worktree or
1522− (a git worktree or submodule). It requires `user` (§7.9).
936+ submodule). It requires `user` (§7.9).
1523−- **`clone URL [DIR]`** creates `DIR` and its missing parents (exit 2 if
937+- **`clone URL [DIR]`** creates `DIR` (exit 2 if it exists and is non-empty),
1524− `DIR` exists and is non-empty, or cannot be created, which includes a `DIR`
1525− such as `new/..` that names an existing directory once its parents exist),
1526 clones `URL` into it as a colocated repository, sets `origin` to `URL`,
938 clones `URL` into it as a colocated repository, sets `origin` to `URL`,
1527− fetches, and creates a working-copy commit as a child of the remote's
939+ fetches, and creates an empty working-copy commit as a child of the
1528− default bookmark target (or of the root commit if the remote has none),
940+ remote's default bookmark target (or of the root commit if the remote has
1529− holding that target's files (see below), and checks it out. The default
1530− bookmark is the branch the remote's `HEAD` names. A clone that fails, its
1531− checkout included, or that is interrupted while it fetches (§1.3), records
1532− nothing and removes the `DIR` it created and each parent it created that is
1533− then empty, or empties the `DIR` it found empty, and nothing else. It
1534− removes or empties each only while its path still leads to that directory,
1535− so a symlink put in its place, or in place of a directory above it, is not
1536− followed: a path that leads elsewhere is left, and the error names it.
1537− Without `DIR`, it is the repository's name in `URL`, as `git clone` names
1538− it: the last component of `URL`'s path, a final `.git` component skipped,
1539− minus `.git` (`repo` for `/srv/repo/.git`, `host:repo.git` and
1540− `https://host/team/repo`); a `URL` that leaves no name, such as `/` or
1541− `..`, is a usage error asking for `DIR`. It requires `user`. Interactive
941+ none). It requires `user`.
1542− confirmation includes a shell-quoted command to enter the clone directory
1543− and run `j status`; cloning cannot change the caller's working directory.
1544- **`remote URL`** sets the URL of `origin`, creating the remote if it does not
942- **`remote URL`** sets the URL of `origin`, creating the remote if it does not
1545 exist. It does not fetch.
943 exist. It does not fetch.
1546
944
1547−`clone` and `remote` store a `URL` that is a local path (one with no scheme
1548−and no `host:` before its first `/`) as the absolute path it names from the
1549−current directory, symlinks resolved, as `jj git clone` does (`git clone`
1550−stores it absolute too), so that `fetch` and `push` reach it from wherever
1551−they run. A local path that begins with `~` (`~/x.git`, `~user/x.git`) is
1552−stored as given, since git expands it to that home directory on every
1553−`fetch` and `push`; `./~/x.git` names a directory called `~`. Any other
1554−`URL` is stored unresolved. A `URL` that cannot be parsed, or a path that
1555−cannot be resolved or whose absolute path is not UTF-8, is a usage error
1556−(exit 2) that changes nothing. `fetch` and `push` run git from the top of
1557−the workspace, as git runs from the top of its working tree, so a relative
1558−local path stored some other way (by `git remote add`, say) is resolved
1559−from there, whichever directory they run in.
1560−
1561−Besides jj's own record of the new workspace, `init` and `clone` each record
1562−at most one operation, described `init` and `clone URL` (`URL` as given, a
1563−local path as stored), holding what they imported and the working-copy
1564−commit; `undo` never undoes it (§7.7). That commit holds its parent's files
1565−less any at a path a checkout cannot create (§7.5 step 1), such as a
1566−committed `.jj` directory: their removal is its change.
1567−
1568### 7.9 Identity
945### 7.9 Identity
1569
946
1570Commits are authored and committed as `user.name <user.email>` from
947Commits are authored and committed as `user.name <user.email>` from
1571`config.j` (§6.3). `j` reads no other configuration; jj-lib's settings object
948`config.j` (§6.3). `j` reads no other configuration; jj-lib's settings object
1572is constructed by `j` from `user` and fixed defaults. `init`, `clone`, and
949is constructed by `j` from `user` and fixed defaults. `init`, `clone`, and
1573persistence exit 3 if `user` is missing or malformed, since that is a
950persistence exit 3 if `user` is missing or malformed, since that is a
1574−configuration error. Authoring also refuses blank fields or the shipped
951+configuration error.
1575−`Your Name` / `you@example.com` placeholders (§6.2), with instructions to
1576−edit the actual config file. Read-only expressions and unchanged results
1577−remain available with placeholder identity. Successful interactive setup
1578−reports the configured identity; terminal control characters are escaped
1579−in confirmations without modifying the identity stored in commits.
1580−`validate` checks the author identity when its argument differs from the
1581−loaded repository, so validated previews report the same identity refusal
1582−before saving. That refusal is a catchable crash, like other `validate`
1583−failures. An unchanged `validate` and previews without `validate` remain usable.
1584
952
1585### 7.10 External diff rendering
953### 7.10 External diff rendering
1586
954
1587`difft p a b` runs difftastic. It is the only builtin that executes another
955`difft p a b` runs difftastic. It is the only builtin that executes another
1588program.
956program.
1589
957
1590−1. Write the content of `a` and `b` to files in separate `old` and `new`
958+1. Write the content of `a` and `b` to two temporary files whose names end in
1591− subdirectories of a fresh temporary directory. Each file keeps the last
959+ the last component of `p` (so difftastic's language detection sees the
1592− component of `p` unchanged, so valid names stay within the filesystem's
960+ real filename), inside a fresh temporary directory.
1593− length limit and difftastic sees the real filename. A last component
1594− that is not a filename is a crash; an empty path uses `file`.
15952. Run `difft OLD NEW` with the process's stdin closed and its environment
9612. Run `difft OLD NEW` with the process's stdin closed and its environment
1596 inherited unchanged, except: if `DFT_COLOR` is unset and `j`'s stdout is a
962 inherited unchanged, except: if `DFT_COLOR` is unset and `j`'s stdout is a
1597 terminal, set `DFT_COLOR=always`; if `DFT_WIDTH` is unset and `j`'s stdout
963 terminal, set `DFT_COLOR=always`; if `DFT_WIDTH` is unset and `j`'s stdout
1598 is a terminal, set it to the terminal width.
964 is a terminal, set it to the terminal width.
1599−3. Capture stdout; the program's stderr is `j`'s own, so what it says there
1600− reaches the user. Delete the temporary directory.
965+3. Capture stdout. Delete the temporary directory.
16014. Return stdout as `Text`. A non-zero exit status is not an error (difftastic
9664. Return stdout as `Text`. A non-zero exit status is not an error (difftastic
1602− uses it to signal "differences found" under some options), so what a
967+ uses it to signal "differences found" under some options); a failure to
1603− `difft` that fails printed, even nothing, is the result; a failure to
1604− execute the program because it is absent uses the fallback below. Other
968+ execute the program is a crash naming it.
1605− launch failures are crashes naming the tool and the OS error.
1606−
1607−If the executable is absent (`NotFound`), return a unified diff with three
1608−lines of context and `--- a/PATH` / `+++ b/PATH` headers using the complete
1609−path `p`. Paths containing whitespace or control characters are quoted.
1610−Content that is not UTF-8 or contains NUL receives a binary-difference summary.
1611−Equal bytes produce empty output. This fallback is usable without installing
1612−another program or changing configuration.
1613
969
1614All rendering options are difftastic's own, set through its `DFT_*`
970All rendering options are difftastic's own, set through its `DFT_*`
1615environment variables. `j` adds no options of its own. `diff` remains
971environment variables. `j` adds no options of its own. `diff` remains
1616−available as a dependency-free unified diff without path headers; the
972+available as a dependency-free unified diff; the reference `config.j` uses
1617−reference `config.j` uses `difft` for display and receives the automatic
973+`difft` for display, so a user without difftastic edits one line.
1618−fallback when difftastic is absent.
1619
974
1620−Packaging: difftastic is an optional runtime enhancement; on Nix, wrap `j`
975+Packaging: difftastic is a runtime dependency; on Nix, wrap `j` with `difft`
1621−with `difft` on its `PATH`.
976+on its `PATH`.
1622
977
1623### 7.11 Tree rendering
978### 7.11 Tree rendering
1624
979
@@ -1642,8 +997,7 @@ run — have no metadata: their margin cells are blank, their size bar is
1642computed from the value, and they sort last among their siblings. The same
997computed from the value, and they sort last among their siblings. The same
1643holds for a `Commit` block in display (§5.1).
998holds for a `Commit` block in display (§5.1).
1644
999
1645−Missing fields crash. `color = "auto"` means colour iff stdout is a terminal
1000+Missing fields crash. `color = "auto"` means colour iff stdout is a terminal.
1646−and `TERM` is not `dumb`.
1647If the environment variable `NO_COLOR` is set and non-empty, colour is never
1001If the environment variable `NO_COLOR` is set and non-empty, colour is never
1648used regardless of `color`. `config.j` fixes the defaults by defining `tree`.
1002used regardless of `color`. `config.j` fixes the defaults by defining `tree`.
1649
1003
@@ -1674,9 +1028,8 @@ Columns, left to right:
16744. **Id**: shortest prefix unique among visible commits, minimum 4.
10284. **Id**: shortest prefix unique among visible commits, minimum 4.
16755. **Size bar** (`detail ≥ 1`, focus and its parent and children only; all
10295. **Size bar** (`detail ≥ 1`, focus and its parent and children only; all
1676 commits at `detail = 2`): one of `▁ ▂ ▃ ▅ ▇`, lines added plus removed
1030 commits at `detail = 2`): one of `▁ ▂ ▃ ▅ ▇`, lines added plus removed
1677− by a shortest line diff against the parent, thresholds 1, 10, 50, 200,
1031+ against the parent, thresholds 1, 10, 50, 200, 1000. Absent for empty
1678− 1000. Absent for empty commits. A changed path with no line added or
1032+ commits.
1679− removed (an empty file, a new file type) counts as one line.
16806. **Message**: first line; nothing if empty.
10336. **Message**: first line; nothing if empty.
16817. **Labels**: right-aligned column, present only if any rendered commit has
10347. **Labels**: right-aligned column, present only if any rendered commit has
1682 a label; names separated by two spaces.
1035 a label; names separated by two spaces.
@@ -1744,7 +1097,7 @@ Messages are never coloured except dim (cousins) and dim italic (empty).
1744
1097
1745These hold for the reference `config.j` and should be property-tested against
1098These hold for the reference `config.j` and should be property-tested against
1746an in-memory implementation of the builtins (§10). `=` means the results are
1099an in-memory implementation of the builtins (§10). `=` means the results are
1747−equal, with snapshots compared path by path (§7.3), or both crash.
1100+equal, or both crash.
1748
1101
1749```
1102```
1750by r.root.id r = r
1103by r.root.id r = r
@@ -1861,10 +1214,6 @@ Drop = { delete : Label } -- delete the remote's bookmark
1861------------------------------------------------------------------------------
1214------------------------------------------------------------------------------
1862
1215
1863(.) : (b -> c) -> (a -> b) -> a -> c -- composition, right to left
1216(.) : (b -> c) -> (a -> b) -> a -> c -- composition, right to left
1864−-- Return a value unchanged. As a repository command, `j id` records the
1865−-- snapshotted working-copy edits in the focused commit. `j save` is the
1866−-- descriptive alias in this reference config; inspections such as `j status`
1867−-- and `j tree` do not record those edits.
1868id : a -> a
1217id : a -> a
1869const : a -> b -> a
1218const : a -> b -> a
1870crash : Text -> a -- abort the program with a message
1219crash : Text -> a -- abort the program with a message
@@ -1926,39 +1275,23 @@ splitOn : Text -> Text -> [Text] -- splitOn "/" "a/b" = ["a" "b"]
1926------------------------------------------------------------------------------
1275------------------------------------------------------------------------------
1927
1276
1928-- Replay a change onto a snapshot: "onto, with the change from `from` to `to`
1277-- Replay a change onto a snapshot: "onto, with the change from `from` to `to`
1929−-- applied." Where the change collides with what is already there, the path's
1278+-- applied." Total: where the change collides with what is already there, the
1930−-- blob is unresolved rather than an error, except that a collision making a
1279+-- path's blob is unresolved rather than an error.
1931−-- path a file on one side and a directory on another, as adding ./a/b onto a
1932−-- file ./a would, crashes, naming the path (§7.3).
1933replay : Snapshot -> Change -> Snapshot
1280replay : Snapshot -> Change -> Snapshot
1934unresolved : Blob -> Bool
1281unresolved : Blob -> Bool
1935−
1936−-- Paths edited in the working copy since the last recorded repository state.
1937−-- `j status` includes this list; `j save` records it in the focused commit.
1938−-- This measures the working copy, even when composed after an in-memory edit.
1939−unrecorded : Repo -> [Path]
1940−touchedPaths : Change -> [Path] -- paths whose content differs, one pass;
1941− -- blobs compared by content id, bytes never read
1942blob : Text -> Blob -- a resolved regular file
1282blob : Text -> Blob -- a resolved regular file
1943−blobHex : Text -> Text -> Blob -- kind (regular/executable/symlink), hex bytes
1944text : Blob -> Text -- its content (markers if unresolved)
1283text : Blob -> Text -- its content (markers if unresolved)
1945by : Id -> Repo -> Repo -- focus the commit with this id; crash if absent
1284by : Id -> Repo -> Repo -- focus the commit with this id; crash if absent
1946−lookupCommit : Id -> Repo -> Commit -- indexed commit lookup without moving the focus
1947−lookupContent : Path -> Snapshot -> Blob -- indexed file content; an empty blob if absent
1948−subtreeCommits : a -> [Commit] -- every commit of a tree in preorder, in one
1949− -- pass: what `commits` (below) is
1950meta : Id -> Meta -- git hash, author, email, committer time;
1285meta : Id -> Meta -- git hash, author, email, committer time;
1951 -- crash for a commit not yet persisted
1286 -- crash for a commit not yet persisted
1952validate : Repo -> Repo -- the repo unchanged, or the crash persisting
1287validate : Repo -> Repo -- the repo unchanged, or the crash persisting
1953− -- it would produce (shape, root, labels, immutability,
1288+ -- it would produce (shape, labels, immutability,
1954 -- focus). `tree . validate . rebase trunk` is an
1289 -- focus). `tree . validate . rebase trunk` is an
1955 -- honest dry run.
1290 -- honest dry run.
1956diff : Blob -> Blob -> Text -- unified diff from the first to the second
1291diff : Blob -> Blob -> Text -- unified diff from the first to the second
1957−difft : Path -> Blob -> Blob -> Text -- difftastic when difft is on PATH; otherwise
1292+difft : Path -> Blob -> Blob -> Text -- difftastic's rendering; needs difft on PATH.
1958− -- unified diff with path headers and 3 context lines,
1293+ -- Options via DFT_* environment variables.
1959− -- or a binary-files summary. DFT_* configures difft.
1960−treeWith : { detail : Int, margin : Bool, elide : Bool, icons : Bool, color : Text
1294+treeWith : { detail : Int, margin : Bool, elide : Bool, icons : Bool, color : Text }
1961− , lanes : Int, author : Bool, date : Bool, files : Bool }
1962 -> Repo -> Text -- the history as a tree; see the spec
1295 -> Repo -> Text -- the history as a tree; see the spec
1963
1296
1964-- Every subvalue of a given shape, however deeply nested. A shape is a type
1297-- Every subvalue of a given shape, however deeply nested. A shape is a type
@@ -1996,11 +1329,8 @@ top = \repo -> top (up repo) or repo
1996
1329
1997-- Every commit of a subtree (or of the focused subtree of a Repo), in preorder.
1330-- Every commit of a subtree (or of the focused subtree of a Repo), in preorder.
1998-- Takes anything with `root` and `children`, so the signature is left open.
1331-- Takes anything with `root` and `children`, so the signature is left open.
1999−-- The builtin is \t -> t.root :: (concat (map commits t.children) or [])
2000−-- in one pass; written that way it copies the list below every commit, and
2001−-- so takes time quadratic in the length of the history.
2002commits : a -> [Commit]
1332commits : a -> [Commit]
2003−commits = subtreeCommits
1333+commits = \t -> t.root :: (concat (map commits t.children) or [])
2004
1334
2005
1335
2006------------------------------------------------------------------------------
1336------------------------------------------------------------------------------
@@ -2044,9 +1374,6 @@ restrict = \m -> filter (\e -> m e.path)
2044select : Fileset -> Snapshot -> Snapshot -> Snapshot -- m from a, the rest from b
1374select : Fileset -> Snapshot -> Snapshot -> Snapshot -- m from a, the rest from b
2045select = \m a b -> restrict m a ++ restrict (neg m) b
1375select = \m a b -> restrict m a ++ restrict (neg m) b
2046
1376
2047−-- Unresolved paths in a snapshot. For the focus, inspect `j status` or
2048−-- `j conflicted . files`, edit the listed files to remove conflict markers,
2049−-- then run `j save` and inspect `j status` again. See `j help conflicts`.
2050conflicted : Snapshot -> [Path]
1377conflicted : Snapshot -> [Path]
2051conflicted = \s -> map (.path) (filter (\e -> unresolved e.content) s)
1378conflicted = \s -> map (.path) (filter (\e -> unresolved e.content) s)
2052
1379
@@ -2095,7 +1422,7 @@ descendants : Revset -- includes the
2095descendants = \repo -> map (.id) (commits repo)
1422descendants = \repo -> map (.id) (commits repo)
2096
1423
2097ancestors : Revset -- includes the focus
1424ancestors : Revset -- includes the focus
2098−ancestors = \repo -> repo.root.id :: map (\f -> f.parent.id) repo.context
1425+ancestors = \repo -> repo.root.id :: ((let p = up repo in ancestors p) or [])
2099
1426
2100siblings : Revset
1427siblings : Revset
2101siblings = \repo -> filter (\i -> i /= repo.root.id) (kids (up repo)) or []
1428siblings = \repo -> filter (\i -> i /= repo.root.id) (kids (up repo)) or []
@@ -2131,11 +1458,6 @@ minus = \a b repo -> let ys = b repo in filter (\i -> not (member i ys)) (a repo
2131matching : (Commit -> Bool) -> Revset -> Revset
1458matching : (Commit -> Bool) -> Revset -> Revset
2132matching = \p rs repo -> filter (\i -> p (commitAt i repo)) (rs repo)
1459matching = \p rs repo -> filter (\i -> p (commitAt i repo)) (rs repo)
2133
1460
2134−-- Find every commit with unresolved files. `j conflicts` lists candidates;
2135−-- use `j by @ID` to focus a mutable one, then `j status` to find its paths.
2136−-- Edit each conflicted file, remove the conflict markers, and run `j save`.
2137−-- Check `j status` again; `j clean` means the focus has no unresolved files,
2138−-- not that the working copy has no unrecorded edits. `j review` shows the diff.
2139conflicts : Revset
1461conflicts : Revset
2140conflicts = matching (\c -> not (null (conflicted c.files))) all
1462conflicts = matching (\c -> not (null (conflicted c.files))) all
2141
1463
@@ -2144,10 +1466,7 @@ firstOf : [Revset] -> Revset
2144firstOf = \rss repo -> foldl (\acc rs -> if null acc then rs repo else acc) [] rss
1466firstOf = \rss repo -> foldl (\acc rs -> if null acc then rs repo else acc) [] rss
2145
1467
2146-- The main line: whichever of these names the remote has, first one wins.
1468-- The main line: whichever of these names the remote has, first one wins.
2147−-- Empty when no fetched remote label is named main, master, or trunk;
1469+-- Empty in a repository with no remote, in which case nothing is immutable.
2148−-- nothing is then immutable. Configure `j remote URL`, run `j fetch`, and
2149−-- inspect `j tree` to see remote labels. A remote with another default label
2150−-- needs this definition adjusted. Start work with j 'new . goto trunk'.
2151trunk : Revset
1470trunk : Revset
2152trunk = firstOf [%main %master %trunk]
1471trunk = firstOf [%main %master %trunk]
2153
1472
@@ -2163,33 +1482,21 @@ immutable = ancestorsOf trunk
2163-- Focusing. `goto` is the only place a revset must have exactly one member.
1482-- Focusing. `goto` is the only place a revset must have exactly one member.
2164------------------------------------------------------------------------------
1483------------------------------------------------------------------------------
2165
1484
2166−-- Move to the single commit selected by a revset.
2167−-- Example: j 'goto %feature'; use j 'new . goto trunk' to start on the main line.
2168goto : Revset -> Edit
1485goto : Revset -> Edit
2169goto = \rs repo ->
1486goto = \rs repo ->
2170 let is = rs repo
1487 let is = rs repo
2171− candidate = \i ->
2172− let c = commitAt i repo
2173− message = head (splitOn "\n" c.message)
2174− in " " ++ show i ++ " " ++ (if message == "" then "(no description)" else message) ++ "\n"
2175 in if length is == 1
1488 in if length is == 1
2176 then by (head is) repo
1489 then by (head is) repo
2177− else if null is
1490+ else crash ("expected one revision, got " ++ show (length is))
2178− then crash "expected one revision, got 0; no matching revision. Run `j tree` to inspect available revisions. For a missing remote label such as %main or trunk, configure `j remote URL`, run `j fetch`, and check the label names in `j tree`."
2179− else crash ("expected one revision, got " ++ show (length is) ++ "\nCandidates:\n" ++ concat (map candidate (take 8 is)) ++ (if length is > 8 then " More candidates: run `j tree`.\n" else "") ++ "Choose a mutable commit with `j by @ID`, or start a child with `j 'new . by @ID'`; replace @ID with a candidate above.")
2180
1491
2181commitAt : Id -> Repo -> Commit
1492commitAt : Id -> Repo -> Commit
2182−commitAt = lookupCommit
1493+commitAt = \i repo -> (by i repo).root
2183
1494
2184prev : Edit -- the parent
1495prev : Edit -- the parent
2185prev = goto parents
1496prev = goto parents
2186
1497
2187−-- Move to the only child. At a tip, `j new` starts a child; when there are
1498+next : Edit -- the only child
2188−-- several children, choose one of the candidates with `j by @ID`.
2189−next : Edit
2190−next = \repo -> if null (kids repo)
2191− then crash "next: this commit has no children; run `j new` to start a new child, or `j tree` to inspect the history"
2192− else goto kids repo
1499+next = goto kids
2193
1500
2194tip : Edit -- end of the line of only-children
1501tip : Edit -- end of the line of only-children
2195tip = \repo -> tip (next repo) or repo
1502tip = \repo -> tip (next repo) or repo
@@ -2215,8 +1522,6 @@ eachChild = forEach kids
2215guard : (Repo -> Bool) -> Edit -- crash unless p holds
1522guard : (Repo -> Bool) -> Edit -- crash unless p holds
2216guard = \p repo -> if p repo then repo else crash "guard"
1523guard = \p repo -> if p repo then repo else crash "guard"
2217
1524
2218−-- Whether the focused commit has no unresolved files. This says nothing
2219−-- about unrecorded working-copy edits; inspect those with `j unrecorded`.
2220clean : Repo -> Bool
1525clean : Repo -> Bool
2221clean = \repo -> null (conflicted repo.root.files)
1526clean = \repo -> null (conflicted repo.root.files)
2222
1527
@@ -2255,28 +1560,11 @@ rewrite = \f repo ->
2255-- Commands
1560-- Commands
2256------------------------------------------------------------------------------
1561------------------------------------------------------------------------------
2257
1562
2258−-- Record the current working-copy edits in the focused commit.
2259−-- Example: j save; use j status first to inspect unrecorded paths.
2260−-- This is id restricted to a Repo: the final Repo result triggers recording.
2261−-- It does not publish anything. `j new` starts a child; `j id` records too.
2262−save : Edit
2263−save = id
2264−
2265−-- Preview an edit and validate that its result could be recorded, then show
2266−-- the resulting tree. This returns Text, so it records nothing.
2267−-- Examples: j 'preview squash'; j 'preview (rebase trunk)'.
2268−-- This helper is in the reference config. Existing custom configs are not
2269−-- upgraded automatically; add it there or use `tree . validate . EDIT`.
2270−preview : Edit -> Repo -> Text
2271−preview = \edit -> tree . validate . edit
2272−
2273-- Set the message of the focused commit.
1563-- Set the message of the focused commit.
2274−-- Example: j 'describe "fix login"'
2275describe : Text -> Edit
1564describe : Text -> Edit
2276describe = \m -> mapRoot (\c -> c { message = m })
1565describe = \m -> mapRoot (\c -> c { message = m })
2277
1566
2278-- Start an empty child of the focus and move to it.
1567-- Start an empty child of the focus and move to it.
2279−-- Examples: j new; j 'describe "next change" . new'
2280new : Edit
1568new : Edit
2281new = \repo -> addChild (newCommit repo.root.files) repo
1569new = \repo -> addChild (newCommit repo.root.files) repo
2282
1570
@@ -2284,7 +1572,6 @@ new = \repo -> addChild (newCommit repo.root.files) repo
2284-- new empty commit on that parent becomes the focus (the parent may be
1572-- new empty commit on that parent becomes the focus (the parent may be
2285-- immutable, and the focus never is). Refused for a commit the remote has a
1573-- immutable, and the focus never is). Refused for a commit the remote has a
2286-- name for: push something else under that name first, or forget the name.
1574-- name for: push something else under that name first, or forget the name.
2287−-- Example: j abandon; validate a preview first with j 'preview abandon'.
2288abandon : Edit
1575abandon : Edit
2289abandon = \repo ->
1576abandon = \repo ->
2290 let c = repo.root
1577 let c = repo.root
@@ -2294,20 +1581,17 @@ abandon = \repo ->
2294 else new (remove (eachChild (replayOnto p) repo))
1581 else new (remove (eachChild (replayOnto p) repo))
2295
1582
2296-- Push the files matching m from the focus down into its parent.
1583-- Push the files matching m from the focus down into its parent.
2297−-- Example: j 'contract (ext "rs")'
2298contract : Fileset -> Edit
1584contract : Fileset -> Edit
2299contract = \m repo -> let a = repo.root
1585contract = \m repo -> let a = repo.root
2300 in at parents (rewrite (\q -> q { files = select m a.files q.files })) repo
1586 in at parents (rewrite (\q -> q { files = select m a.files q.files })) repo
2301
1587
2302-- Fold the focus entirely into its parent; a new empty commit becomes the focus.
1588-- Fold the focus entirely into its parent; a new empty commit becomes the focus.
2303−-- Example: j squash; preview first with j 'preview squash'.
2304squash : Edit
1589squash : Edit
2305squash = abandon . contract everything
1590squash = abandon . contract everything
2306
1591
2307-- Push the files matching m from the focus up into a new child, which becomes
1592-- Push the files matching m from the focus up into a new child, which becomes
2308-- the focus and inherits the old focus's children. Labels stay with the
1593-- the focus and inherits the old focus's children. Labels stay with the
2309-- original commit, since they are the remote's.
1594-- original commit, since they are the remote's.
2310−-- Example: j 'split (ext "rs")'; use (under ./src) to select a directory.
2311split : Fileset -> Edit
1595split : Fileset -> Edit
2312split = \m repo ->
1596split = \m repo ->
2313 let a = repo.root
1597 let a = repo.root
@@ -2317,7 +1601,6 @@ split = \m repo ->
2317 in by x.id (repo { root = a', children = [{ root = x, children = repo.children }] })
1601 in by x.id (repo { root = a', children = [{ root = x, children = repo.children }] })
2318
1602
2319-- Move the focused subtree onto another commit. Returns to the moved commit.
1603-- Move the focused subtree onto another commit. Returns to the moved commit.
2320−-- Example: j 'rebase trunk'; preview with j 'preview (rebase trunk)'.
2321rebase : Revset -> Edit
1604rebase : Revset -> Edit
2322rebase = \rs repo ->
1605rebase = \rs repo ->
2323 let d = (goto rs repo).root
1606 let d = (goto rs repo).root
@@ -2328,7 +1611,6 @@ rebase = \rs repo ->
2328 in by me (attach x.subtree (by d.id x.rest))
1611 in by me (attach x.subtree (by d.id x.rest))
2329
1612
2330-- Apply the focused commit's change onto another commit, as a new child there.
1613-- Apply the focused commit's change onto another commit, as a new child there.
2331−-- Example: j 'pick %main'
2332pick : Revset -> Edit
1614pick : Revset -> Edit
2333pick = \rs repo ->
1615pick = \rs repo ->
2334 let ch = changeOf repo
1616 let ch = changeOf repo
@@ -2336,7 +1618,6 @@ pick = \rs repo ->
2336 in at rs (addChild (newCommit (replay t.files ch))) repo
1618 in at rs (addChild (newCommit (replay t.files ch))) repo
2337
1619
2338-- A new child of the focus that undoes the change of the given commit.
1620-- A new child of the focus that undoes the change of the given commit.
2339−-- Example: j 'backout %feature'
2340backout : Revset -> Edit
1621backout : Revset -> Edit
2341backout = \rs repo ->
1622backout = \rs repo ->
2342 let ch = changeOf (goto rs repo)
1623 let ch = changeOf (goto rs repo)
@@ -2382,53 +1663,40 @@ entryAt = \p s -> filter (\e -> e.path == p) s
2382
1663
2383-- The content at path p, or an empty file if absent.
1664-- The content at path p, or an empty file if absent.
2384contentAt : Path -> Snapshot -> Blob
1665contentAt : Path -> Snapshot -> Blob
2385−contentAt = lookupContent
1666+contentAt = \p s -> (head (entryAt p s)).content or blob ""
2386
1667
2387-- The paths a change touches.
1668-- The paths a change touches.
2388touched : Change -> [Path]
1669touched : Change -> [Path]
2389−touched = touchedPaths
1670+touched = \ch ->
1671+ let paths = map (.path) ch.to ++ filter (\p -> not (member p (map (.path) ch.to))) (map (.path) ch.from)
1672+ in filter (\p -> entryAt p ch.to /= entryAt p ch.from) paths
2390
1673
2391−-- Paths changed by the focused commit compared with its parent, including
1674+-- Paths whose content differs between the focus and its parent.
2392−-- the working-copy snapshot. These can remain nonempty after `j save`;
2393−-- `j unrecorded` shows only working-copy edits not yet recorded.
2394−-- Example: j changed
2395changed : Repo -> [Path]
1675changed : Repo -> [Path]
2396changed = \repo -> touched (changeOf repo)
1676changed = \repo -> touched (changeOf repo)
2397
1677
2398−-- What the focus changes, one record per changed path. Uses difftastic when
1678+-- What the focus changes, rendered by difftastic, one record per changed path.
2399−-- installed; a missing difft falls back to a unified diff with path headers
1679+-- Replace difft with (\p -> diff) to use the built-in unified diff instead.
2400−-- and 3 context lines, or a binary-files summary.
2401diffs : Repo -> [{ path : Path, diff : Text }]
1680diffs : Repo -> [{ path : Path, diff : Text }]
2402diffs = \repo ->
1681diffs = \repo ->
2403 let ch = changeOf repo
1682 let ch = changeOf repo
2404 in map (\p -> { path = p, diff = difft p (contentAt p ch.from) (contentAt p ch.to) }) (touched ch)
1683 in map (\p -> { path = p, diff = difft p (contentAt p ch.from) (contentAt p ch.to) }) (touched ch)
2405
1684
2406-- The same, as one page of text. A Text result prints raw, so `j review` is
1685-- The same, as one page of text. A Text result prints raw, so `j review` is
2407−-- readable in the terminal. A focus that changes nothing reads "no changes";
1686+-- readable in the terminal.
2408−-- missing difft uses the built-in unified diff (path headers, 3 context lines)
2409−-- or a binary-files summary. Other execution errors remain visible; an
2410−-- installed difft's output, even nothing, is the page.
2411−-- Example: j review; difftastic is optional.
2412review : Repo -> Text
1687review : Repo -> Text
2413−review = \repo ->
1688+review = \repo -> concat (map (\d -> d.diff) (diffs repo)) or "no changes\n"
2414− let ds = diffs repo
1689+
2415− in if null ds then "no changes\n" else concat (map (\d -> d.diff) ds)
2416−
2417−-- A summary of the focus. `changed` lists changes in the commit compared
1690+-- A summary of the focus.
2418−-- with its parent; `unrecorded` lists working-copy edits not yet recorded.
2419−-- Inspection does not record anything. Run `j save` to record those edits.
2420−-- Example: j status
2421−status : Repo -> { id : Id, message : Text, labels : [Label], changed : [Path], conflicts : [Path], unrecorded : [Path] }
1691+status : Repo -> { id : Id, message : Text, labels : [Label], changed : [Path], conflicts : [Path] }
2422status = \repo ->
1692status = \repo ->
2423 { id = repo.root.id
1693 { id = repo.root.id
2424 , message = repo.root.message
1694 , message = repo.root.message
2425 , labels = repo.root.labels
1695 , labels = repo.root.labels
2426 , changed = changed repo
1696 , changed = changed repo
2427− , conflicts = conflicted repo.root.files
1697+ , conflicts = conflicted repo.root.files }
2428− , unrecorded = unrecorded repo }
2429
1698
2430-- A summary of every commit, top first, in preorder.
1699-- A summary of every commit, top first, in preorder.
2431−-- Example: j log
2432log : Repo -> [{ id : Id, message : Text, labels : [Label], focus : Bool }]
1700log : Repo -> [{ id : Id, message : Text, labels : [Label], focus : Bool }]
2433log = \repo ->
1701log = \repo ->
2434 map (\c -> { id = c.id
1702 map (\c -> { id = c.id
@@ -2439,21 +1707,11 @@ log = \repo ->
2439
1707
2440-- The history as a tree. Edit these records to change what `j tree` shows.
1708-- The history as a tree. Edit these records to change what `j tree` shows.
2441-- (The parentheses are required: `f { … }` would be a record update of f.)
1709-- (The parentheses are required: `f { … }` would be a record update of f.)
2442−-- The default `tree` shows every commit, sorted by time (parents always above
2443−-- their children); `treeCompact` folds uninteresting runs and far subtrees.
2444−-- Examples: j tree; j 'tree . squash' previews an edit without saving it.
2445tree : Repo -> Text
1710tree : Repo -> Text
2446−tree = treeWith ({ detail = 1, margin = false, elide = false, icons = false, color = "auto", lanes = 4, author = false, date = false, files = false })
1711+tree = treeWith ({ detail = 1, margin = false, elide = true, icons = false, color = "auto" })
2447−
2448−treeCompact : Repo -> Text
2449−treeCompact = treeWith ({ detail = 1, margin = false, elide = true, icons = false, color = "auto", lanes = 4, author = false, date = false, files = false })
2450
1712
2451treeFull : Repo -> Text
1713treeFull : Repo -> Text
2452−treeFull = treeWith ({ detail = 2, margin = true, elide = false, icons = false, color = "auto", lanes = 4, author = false, date = false, files = false })
1714+treeFull = treeWith ({ detail = 2, margin = true, elide = false, icons = false, color = "auto" })
2453−
2454−-- The full tree plus the extra data columns: date, files changed, author.
2455−treeData : Repo -> Text
2456−treeData = treeWith ({ detail = 2, margin = true, elide = false, icons = false, color = "auto", lanes = 4, author = true, date = true, files = true })
2457```
1715```
2458
1716
2459---
1717---
@@ -2461,15 +1719,13 @@ treeData = treeWith ({ detail = 2, margin = true, elide = false, icons = false,
2461## 10. Testing
1719## 10. Testing
2462
1720
2463The interpreter must be testable without a jj repository. Provide an in-memory
1721The interpreter must be testable without a jj repository. Provide an in-memory
2464−implementation of the domain builtins (`replay`, `unresolved`, `blob`, `blobHex`,
1722+implementation of the domain builtins (`replay`, `unresolved`, `blob`,
2465`text`, `by`, `extract`, `meta`, `difft` as a stub, and the minting
1723`text`, `by`, `extract`, `meta`, `difft` as a stub, and the minting
2466−of `@`) in which a resolved blob carries bytes and a file kind, a
1724+of `@`) in which a blob is text, a
2467conflict is a record of the three sides, and `replay` resolves a path exactly
1725conflict is a record of the three sides, and `replay` resolves a path exactly
2468−when at most one side changed it or both made the same change, path by path
1726+when at most one side changed it or both made the same change. Property-test
2469−even where a file meets a directory, which jj merges as one value (§7.3).
2470−Property-test the laws of §8 over randomly generated `Repo` values (bounded
1727+the laws of §8 over randomly generated `Repo` values (bounded depth and width,
2471−depth and width, random labels, random file edits) using the reference
1728+random labels, random file edits) using the reference `config.j` unmodified.
2472−`config.j` unmodified.
2473
1729
2474The builtin `by` must agree, on every generated repo, with this in-language
1730The builtin `by` must agree, on every generated repo, with this in-language
2475reference definition, which uses positional moves the public base does not
1731reference definition, which uses positional moves the public base does not
@@ -2499,10 +1755,9 @@ here is out of scope (§12). Nothing here uses the `jj` binary.
2499**Getting a repository**
1755**Getting a repository**
2500
1756
2501```
1757```
2502−j setup 'Your actual name' you@your-domain.example -- create the full config once
1758+$EDITOR ~/.config/j/config.j -- set `user` once
2503j init -- new repository here, or adopt an existing .git
1759j init -- new repository here, or adopt an existing .git
2504−j clone git@host:org/repo.git project -- or clone an existing one
1760+j clone git@host:org/repo.git -- or an existing one
2505−cd project -- after clone, enter its directory
2506j remote git@host:org/repo.git -- attach a remote to an init'd repo
1761j remote git@host:org/repo.git -- attach a remote to an init'd repo
2507```
1762```
2508
1763
@@ -2511,9 +1766,9 @@ j remote git@host:org/repo.git -- attach a remote to an init'd repo
2511```
1766```
2512j tree -- the history as a tree, focus marked
1767j tree -- the history as a tree, focus marked
2513j treeFull -- the same with sizes, ages, authors, changed paths
1768j treeFull -- the same with sizes, ages, authors, changed paths
2514−j status -- focus, in-commit changes, unrecorded edits, conflicts
1769+j status -- id, message, labels, changed paths, conflicts of @
2515j log -- every commit as a record
1770j log -- every commit as a record
2516−j review -- difftastic or unified diff of @ against its parent
1771+j review -- difftastic rendering of @ against its parent
2517j review . by @kpq -- the same for another commit, without moving
1772j review . by @kpq -- the same for another commit, without moving
2518j diffs -- the same, one record per path
1773j diffs -- the same, one record per path
2519j here -- @'s id
1774j here -- @'s id
@@ -2544,8 +1799,7 @@ j rebase trunk
2544j guard clean . rebase trunk or id -- only if it comes out clean
1799j guard clean . rebase trunk or id -- only if it comes out clean
2545j pick @kpq
1800j pick @kpq
2546j 'forEach (descendantsOf here) (describe "wip")'
1801j 'forEach (descendantsOf here) (describe "wip")'
2547−j save -- record the working directory, change nothing else
1802+j id -- record the working directory, change nothing else
2548−j 'preview squash' -- validate and show the result without saving
2549j tree . squash -- dry run: show the result, persist nothing
1803j tree . squash -- dry run: show the result, persist nothing
2550j tree . validate . squash -- the same, refusing what persistence would refuse
1804j tree . validate . squash -- the same, refusing what persistence would refuse
2551j squash && j tree -- do, then look
1805j squash && j tree -- do, then look
@@ -2567,10 +1821,7 @@ j rebase trunk -- bring the stack up to date
2567A rebase or fetch that conflicts leaves unresolved files in the affected
1821A rebase or fetch that conflicts leaves unresolved files in the affected
2568commits. `j conflicts` lists them; `j by @…` moves there, which checks the
1822commits. `j conflicts` lists them; `j by @…` moves there, which checks the
2569files out with conflict markers; editing the files and running any persisting
1823files out with conflict markers; editing the files and running any persisting
2570−expression (`j save` or `j id` at minimum) records the resolution. Run
1824+expression (`j id` at minimum) records the resolution.
2571−`j status` to verify `No conflicts` and `All edits recorded` in the focus,
2572−and `j conflicts` to check the rest of the history. No continue command is
2573−needed.
2574
1825
2575**Mistakes**
1826**Mistakes**
2576
1827
@@ -2585,7 +1836,7 @@ j redo
2585Any definition added to `config.j` whose value is an `Edit` is a command:
1836Any definition added to `config.j` whose value is an `Edit` is a command:
2586
1837
2587```haskell
1838```haskell
2588−pr = \n -> describe n . prev . squash
1839+pr = \n -> describe n . squash
2589```
1840```
2590
1841
2591```
1842```
@@ -2598,9 +1849,8 @@ j 'pr "fix login"' && j 'push (label "fix-login" here)'
2598
1849
2599Second parents of merge commits (merges are placed under their first parent
1850Second parents of merge commits (merges are placed under their first parent
2600and are read-only); hunk-level `split`; conflict resolution inside the
1851and are read-only); hunk-level `split`; conflict resolution inside the
2601−language (blobs are opaque); workspaces other than the default; sparse working
1852+language (blobs are opaque); workspaces other than the default; remotes other
2602−copies (refused; §7.1); remotes other than `origin`; tags; submodules and LFS;
1853+than `origin`; tags; submodules and LFS; ignore rules beyond `.gitignore`;
2603−ignore rules beyond `.gitignore`; repairing a repository in which one change
1854+changing file modes or symlinks from the language; repairing a repository in
2604−id has two visible commits (refused; §7.2); general command flags beyond the
1855+which one change id has two visible commits (refused; §7.2); a `--help` flag
2605−`--help` and `-h` discovery arguments; static typing. None of
2606−these are precluded by the design.
1856+or any flag at all; static typing. None of these are precluded by the design.
specs/tree.md+50−94
@@ -113,8 +113,6 @@ After `n` is placed in lane `ℓ`:
113* If `n` was the last child of `p` and did not inherit `p`'s lane (rule 5 with
113* If `n` was the last child of `p` and did not inherit `p`'s lane (rule 5 with
114 `p` the trunk head, or `p` the root of an empty trunk), `p`'s lane becomes
114 `p` the trunk head, or `p` the root of an empty trunk), `p`'s lane becomes
115 empty below this row: the fork on this row is drawn with `╰` instead of `├`.
115 empty below this row: the fork on this row is drawn with `╰` instead of `├`.
116− When `n` finds no lane (§Overflow) no fork is drawn, but `p`'s lane empties
117− all the same; the rail that reaches `n`'s row is drawn there as `│`.
118
116
119**Reservations.** When a node `p ∈ T` is placed and has a trunk child `t`,
117**Reservations.** When a node `p ∈ T` is placed and has a trunk child `t`,
120every side child of `p` that comes *after* `t` in row order needs its own
118every side child of `p` that comes *after* `t` in row order needs its own
@@ -137,16 +135,10 @@ always to the right of its source.
137If rule 5 or a reservation finds no empty lane in `0 < ℓ < lanes`, the
135If rule 5 or a reservation finds no empty lane in `0 < ℓ < lanes`, the
138subtree rooted at `n` is **flattened**: `n` and every descendant of it still
136subtree rooted at `n` is **flattened**: `n` and every descendant of it still
139get their rows, in row order, with their id, message, labels and margin, but
137get their rows, in row order, with their id, message, labels and margin, but
140−they take no lane. Their rails area preserves every rail that passes through.
138+they take no lane. Their rails area shows only the rails that pass through
141−If any rendered node is flattened, a separate four-column status area
139+and `»` in the last column. Nothing is hidden — the focus in particular is
142−is reserved between the rails and the id separator on every row. Flattened
143−rows show `»`; on a commit row this is followed by a space and its normal
144−node glyph (§Glyphs), padded to four display columns. Non-flattened rows
145−leave the area blank. Neither the overflow marker nor a node glyph replaces
146−a live rail. Nothing is hidden —
147−the focus, conflicts, and empty commits in particular are always identifiable
148−— only the shape beyond `lanes` is lost. Widen `lanes` (or enable `elide`) to
140+always shown — only the shape beyond `lanes` is lost. Widen `lanes` (or
149−see it.
141+enable `elide`) to see it.
150
142
151#### Step 4 — drawing a row
143#### Step 4 — drawing a row
152
144
@@ -155,34 +147,26 @@ Columns, left to right, at fixed offsets:
1551. **Gutter**, 2 characters: `▶ ` on the focus row, blank otherwise.
1471. **Gutter**, 2 characters: `▶ ` on the focus row, blank otherwise.
1562. **Rails**, `2 · lanes` characters: lane `i` owns characters `2i` and
1482. **Rails**, `2 · lanes` characters: lane `i` owns characters `2i` and
157 `2i+1`.
149 `2i+1`.
158−3. **Overflow status**, 4 display columns when any rendered node is flattened;
159− absent otherwise (§Overflow).
160−4. One space.
150+3. One space.
161−5. **Id**: `@` followed by the shortest prefix unique among the commits of the
151+4. **Id**: `@` followed by the shortest prefix unique among the commits of the
162 display tree, minimum 4 — so the rendered id is itself a valid id literal
152 display tree, minimum 4 — so the rendered id is itself a valid id literal
163 (§3.1). Blank on a run row.
153 (§3.1). Blank on a run row.
164−6. **Size bar** (`detail ≥ 1`, focus and its parent and children only; all
154+5. **Size bar** (`detail ≥ 1`, focus and its parent and children only; all
165− commits at `detail = 2`): `▁ ▂ ▃ ▅ ▇`, as before. A row without a bar
155+ commits at `detail = 2`): `▁ ▂ ▃ ▅ ▇`, as before.
166− keeps the column blank; it is left out only when no row has a bar.
167−7. **Message**: first line; nothing if empty; followed by `⋯ n` on a
156+6. **Message**: first line; nothing if empty; followed by `⋯ n` on a
168 collapsed node.
157 collapsed node.
169−8. **Labels**: left-aligned column, present only if any rendered commit has
158+7. **Labels**: right-aligned column, present only if any rendered commit has
170 a label; names separated by two spaces.
159 a label; names separated by two spaces.
171−9. **Margin** (`margin = true`): age and author initials, as before, each
160+8. **Margin** (`margin = true`): age and author initials, right-aligned, as
172− right-aligned in its own column (a `9m` ends under a `13m`), so the margin
161+ before.
173− ends at one column on every row.
174−10. **Data columns** (off unless enabled): `date` shows the commit's absolute
162+9. **Data columns** (off unless enabled): `date` shows the commit's absolute
175 date `YYYY-MM-DD`; `files` shows the number of files changed as `n files`;
163 date `YYYY-MM-DD`; `files` shows the number of files changed as `n files`;
176 `author` shows the full author name. They appear in that order, grey,
164 `author` shows the full author name. They appear in that order, grey,
177 immediately before the margin in the same right-aligned block.
165 immediately before the margin in the same right-aligned block.
178
166
179−Columns 5–9 start at the same offset on every row. Labels start two columns
167+Columns 4–8 start at the same offset on every row. When stdout is a terminal
180−past the widest message, and the data columns and the margin start two
181−columns past the widest label (past the widest message when there is no
182−label column). Each data column, the age and the initials are as wide as
183−their widest entry and two columns apart. When stdout is a terminal and a row
184−would exceed its width, the message is cut and ends with `…` so that labels
168+and a row would exceed its width, the message is cut and ends with `…` so that
185−and the margin keep their columns; otherwise nothing is cut.
169+labels and the margin keep their columns; otherwise nothing is cut.
186
170
187**Rails characters.** For lane `i` on the row of node `n` placed in lane
171**Rails characters.** For lane `i` on the row of node `n` placed in lane
188`ℓ`, character `2i` is, first rule that applies:
172`ℓ`, character `2i` is, first rule that applies:
@@ -195,27 +179,19 @@ and the margin keep their columns; otherwise nothing is cut.
195| `i` lies between the two ends of a horizontal segment on this row and holds a rail | `┼` |
179| `i` lies between the two ends of a horizontal segment on this row and holds a rail | `┼` |
196| `i` lies between the two ends of a horizontal segment on this row | `─` |
180| `i` lies between the two ends of a horizontal segment on this row | `─` |
197| `i` holds a rail passing through this row | `│` |
181| `i` holds a rail passing through this row | `│` |
182+| flattened rows only: `i = lanes − 1` | `»` |
198| otherwise | space |
183| otherwise | space |
199
184
200Character `2i+1` is `─` if it lies inside a horizontal segment (from a fork
185Character `2i+1` is `─` if it lies inside a horizontal segment (from a fork
201source to its target, or from a node to its rightmost reservation), else a
186source to its target, or from a node to its rightmost reservation), else a
202space. On a run row the count follows the `╎`, written into the rails area
187space. On a run row the count follows the `╎`, written into the rails area
203−and, if needed, into the empty id column; when lanes to the right of the run
188+and, if needed, into the empty id column.
204−hold rails, it follows the rightmost of them instead, so that it covers no
189+
205−rail. The count is never cut: what does not fit the rails area runs on,
206−unbroken, through the space after it into the id column, which is widened if
207−it cannot hold the rest.
208−
209−**Detail lines** (`detail = 2`, focus only): changed paths appear directly
190+**Detail line** (`detail = 2`, focus only): one extra row directly under the
210−under the focus, with a mark each: `+` added, `~` modified, `−` deleted,
211−`✖` unresolved. Every detail row shows `│` in every lane that holds a rail
191+focus. Its rails show `│` in every lane that holds a rail below the focus row
212−below the focus row (including the focus's own lane if it has children), and
192+(including the focus's own lane if it has children); the text starts two
213−leaves any overflow status column blank. The text starts two columns into the
193+characters past the id column and lists the changed paths with a mark each:
214−id column, under the focus's id, at the same offset on every continuation row.
194+`+` added, `~` modified, `−` deleted, `✖` unresolved.
215−When stdout is a terminal, paths wrap onto continuation rows within the width
216−available after this prefix. A path longer than that width may split across
217−rows; wrapping does not discard any path content. Redirected output keeps
218−the changed paths on a single detail row.
219
195
220#### Glyphs
196#### Glyphs
221
197
@@ -232,13 +208,7 @@ Unchanged from before:
232| `⌂` | root | `🌱` |
208| `⌂` | root | `🌱` |
233
209
234Precedence: `⌂`, then `⊗`, then `◌`, then `◆`, then the position glyphs.
210Precedence: `⌂`, then `⊗`, then `◌`, then `◆`, then the position glyphs.
235−`⊗` and `◌` are properties of the history being drawn — a commit's files, and
211+Rails use `│ ├ ╰ ─ ┼ ╮ ┬ ╎ »`.
236−its parent's in that history — not of the stored commit with the same id, so
237−a dry run (§1.2) draws them as the edit would leave them.
238−An `icons = true` glyph is two columns wide, so it fills both characters of
239−its lane: character `2ℓ+1` of its row is not drawn.
240−Rails use `│ ├ ╰ ─ ┼ ╮ ┬ ╎`; `»` belongs to the separate overflow status
241−column.
242
212
243#### Elision (`elide = true`)
213#### Elision (`elide = true`)
244
214
@@ -262,29 +232,18 @@ drawn with `⌂`.
262
232
263#### Colour
233#### Colour
264
234
265−Glyphs are coloured by meaning (the same precedence as their shape): root bold
235+Glyphs are coloured by meaning (the same precedence as their shape): root bold,
266−magenta, conflict bright red, empty dim, immutable bright blue, focus bold
236+conflict red, empty dim, immutable blue, focus bold cyan, ancestor of focus
267−cyan, ancestor of focus bright green, other bright white. Ids are bold magenta,
237+green, other light grey. Ids keep their existing colours (conflict red,
268−with conflict red and immutable blue taking precedence; the focus id and `▶`
269−gutter are bold cyan. Messages are bold on the focus row, red on a conflict,
238+immutable blue). Messages are bold on the focus row, red on a conflict, and
270−and dim-italic when empty. Labels are bold green; the margin and data columns
239+dim-italic when empty. Labels are green; the margin and the data columns are
271−are grey with bright author-hued initials. Size bars progress from cyan
240+grey with author-hued initials. Rail connectors in lane 0 take the immutable
272−through blue and magenta to yellow as their height increases. Rail connectors
241+colour; connectors in other lanes are dim.
273−in lane 0 take the immutable colour; other lanes cycle through magenta, cyan,
242+
274−yellow, and green by lane position. Glyphs always keep their semantic colour.
275−
276−The whole focus row is highlighted with bright text on a dark-blue background
243+The whole focus row is highlighted with a background band that spans the full
277−band that spans the full terminal width. This is colour only: glyphs, rails,
244+terminal width. This is colour only: glyphs, rails, and the `▶` gutter still
278−and the `▶` gutter still carry the meaning, so a colourless rendering loses
245+carry the meaning, so a colourless rendering loses nothing — the band is a
279−nothing — the band is a no-op when colour is off.
246+no-op when colour is off.
280−
281−Structured value display shares this palette: id prefixes are bold magenta,
282−field names and table headers dim cyan, paths cyan, integers and file sizes
283−yellow, and booleans green for true and red for false. Raw text and blob
284−contents are never styled. `color = "auto"` requires a terminal whose `TERM`
285−is not `dumb`; `color = "always"` explicitly enables colour and `"never"`
286−disables it. A non-empty `NO_COLOR` disables colour in every mode. Redirected
287−output remains plain in auto mode.
288
247
289#### Legend
248#### Legend
290
249
@@ -297,10 +256,8 @@ The legend covers the node glyphs present (§Glyphs), the focus gutter `▶`,
297run `╎ n` and collapsed `⋯ n` rows when shown, and the detail-line marks
256run `╎ n` and collapsed `⋯ n` rows when shown, and the detail-line marks
298(`+` added, `~` modified, `−` deleted, `✖` unresolved) when a detail line is
257(`+` added, `~` modified, `−` deleted, `✖` unresolved) when a detail line is
299present. A collapsed node shows `⋯ n` only when `n > 0`, so the `⋯` legend
258present. A collapsed node shows `⋯ n` only when `n > 0`, so the `⋯` legend
300−entry appears only then. When any rendered node is flattened, it also includes
259+entry appears only then. The rails characters are not itemised; they are
301−`» branch exceeds lane limit; increase lanes`. Node glyphs in the overflow
302−status column contribute their normal legend entries. The rails characters
303−are not itemised; they are structural (§Step 4).
260+structural (§Step 4).
304
261
305The legend text is faint (dim). Placement: when stdout is a terminal and the
262The legend text is faint (dim). Placement: when stdout is a terminal and the
306tree's widest line plus the legend fits the terminal width, the legend is set
263tree's widest line plus the legend fits the terminal width, the legend is set
@@ -333,21 +290,20 @@ Rendered:
333
290
334```
291```
335 ╎ 14
292 ╎ 14
336− ◆─╮ @aaaa ▅ add parser 3w mo
293+ ◆─╮ @aaaa ▅ add parser 3w mo
337− ◆ │ @kpqx ▅ release 1.2 main 2w mo
294+ ◆ │ @kpqx ▅ release 1.2 main 2w mo
338− │ ○ @mnrv ▂ fix lexer 1w mo
295+ │ ○ @mnrv ▂ fix lexer 1w mo
339−▶ ├─⊗ @wqzt ▃ wip feature 2d mo
296+▶ ├─⊗ @wqzt ▃ wip feature 2d mo
340 │ │ ✖ src/lexer.rs ~ src/parser.rs + tests/lexer.rs
297 │ │ ✖ src/lexer.rs ~ src/parser.rs + tests/lexer.rs
341− ╰─┼─◌ @ptlm docs ⋯ 3 1d ak
298+ ╰─┼─◌ @ptlm docs ⋯ 3 1d ak
342− ├─○ @qrst ▁ spike 5h mo
299+ ├─○ @qrst ▁ spike 5h mo
343− ○ @yxsk ▃ 1h mo
300+ ○ @yxsk ▃ 1h mo
344```
301```
345
302
346−Column offsets in this example: gutter 0–1, rails 2–7, id from 9, size bar
303+Column offsets in this example: gutter 0–1, rails 2–7, id from 9. Lane 0 is
347−15, message from 18, labels from 31, margin from 40, and the detail line's
348−marks from 11. Lane 0 is `◆ ◆ │ ├ ╰` from `aaaa` to `ptlm` and then empty;
304+`◆ ◆ │ ├ ╰` from `aaaa` to `ptlm` and then empty; the reservation for `mnrv`
349−the reservation for `mnrv` is drawn on `aaaa`'s row as `─╮` and consumed on
305+is drawn on `aaaa`'s row as `─╮` and consumed on `mnrv`'s row; `ptlm` forks
350−`mnrv`'s row; `ptlm` forks to lane 2 across the live lane 1, hence `┼`.
306+to lane 2 across the live lane 1, hence `┼`.
351
307
352The reference `config.j` defines `tree` with `detail = 1, margin = false,
308The reference `config.j` defines `tree` with `detail = 1, margin = false,
353elide = false` (every commit, time-sorted), `treeCompact` with `elide = true`
309elide = false` (every commit, time-sorted), `treeCompact` with `elide = true`
src/ast.rs+2−35
@@ -29,7 +29,7 @@ pub enum Expr {
29 LabelLit(String),
29 LabelLit(String),
30 PathLit(Vec<String>),
30 PathLit(Vec<String>),
31 Bool(bool),
31 Bool(bool),
32− Lambda(Vec<Pattern>, Rc<Expr>, Source), // params, body, source text
32+ Lambda(Vec<Pattern>, Rc<Expr>, String), // params, body, source text
33 If(Rc<Expr>, Rc<Expr>, Rc<Expr>),
33 If(Rc<Expr>, Rc<Expr>, Rc<Expr>),
34 Let(Vec<(String, Rc<Expr>)>, Rc<Expr>),
34 Let(Vec<(String, Rc<Expr>)>, Rc<Expr>),
35 App(Rc<Expr>, Rc<Expr>),
35 App(Rc<Expr>, Rc<Expr>),
@@ -45,34 +45,6 @@ pub enum Expr {
45 Crash, // internal marker if needed
45 Crash, // internal marker if needed
46}
46}
47
47
48−/// The source text of a lambda or section, exactly as written, which is how
49−/// its closure renders (§5.2): a span of the program text it was parsed
50−/// from. The lambdas of one program share that text instead of each holding
51−/// a copy, so nested lambdas cost a span each, not their text again.
52−#[derive(Clone)]
53−pub struct Source {
54− text: Rc<str>,
55− start: usize,
56− end: usize,
57−}
58−
59−impl Source {
60− /// The bytes `start..end` of `text`, which must fall on char boundaries.
61− pub fn new(text: Rc<str>, start: usize, end: usize) -> Self {
62− Source { text, start, end }
63− }
64−
65− pub fn as_str(&self) -> &str {
66− &self.text[self.start..self.end]
67− }
68−}
69−
70−impl std::fmt::Debug for Source {
71− fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72− write!(f, "{:?}", self.as_str())
73− }
74−}
75−
76#[derive(Debug, Clone)]
48#[derive(Debug, Clone)]
77pub enum Item {
49pub enum Item {
78 TypeDecl(String, TypeExpr),
50 TypeDecl(String, TypeExpr),
@@ -89,18 +61,13 @@ impl Expr {
89 out.insert(n.clone());
61 out.insert(n.clone());
90 }
62 }
91 }
63 }
92− // `%main` is `labelled "main"` (§4.11)
93− Expr::LabelLit(_) => {
94− if !bound.iter().any(|b| b == "labelled") {
95− out.insert("labelled".to_string());
96− }
97− }
98 Expr::TypeName(_)
64 Expr::TypeName(_)
99 | Expr::Int(_)
65 | Expr::Int(_)
100 | Expr::Text(_)
66 | Expr::Text(_)
101 | Expr::IdLit(_)
67 | Expr::IdLit(_)
102 | Expr::Id(_)
68 | Expr::Id(_)
103 | Expr::NewId
69 | Expr::NewId
70+ | Expr::LabelLit(_)
104 | Expr::PathLit(_)
71 | Expr::PathLit(_)
105 | Expr::Bool(_)
72 | Expr::Bool(_)
106 | Expr::SelectorFun(_)
73 | Expr::SelectorFun(_)
src/builtins.rs+79−103
@@ -74,8 +74,8 @@ pub fn all_builtins() -> Vec<(String, Value)> {
74 builtin(">=", 2, b_ge),
74 builtin(">=", 2, b_ge),
75 builtin("show", 1, b_show),
75 builtin("show", 1, b_show),
76 builtin("::", 2, b_cons),
76 builtin("::", 2, b_cons),
77− builtin("map", 2, on_machine),
77+ builtin("map", 2, b_map),
78− builtin("filter", 2, on_machine),
78+ builtin("filter", 2, b_filter),
79 builtin("length", 1, b_length),
79 builtin("length", 1, b_length),
80 builtin("null", 1, b_null),
80 builtin("null", 1, b_null),
81 builtin("head", 1, b_head),
81 builtin("head", 1, b_head),
@@ -86,7 +86,7 @@ pub fn all_builtins() -> Vec<(String, Value)> {
86 builtin("drop", 2, b_drop),
86 builtin("drop", 2, b_drop),
87 builtin("member", 2, b_member),
87 builtin("member", 2, b_member),
88 builtin("range", 2, b_range),
88 builtin("range", 2, b_range),
89− builtin("foldl", 3, on_machine),
89+ builtin("foldl", 3, b_foldl),
90 builtin("concat", 1, b_concat),
90 builtin("concat", 1, b_concat),
91 builtin("++", 2, b_append),
91 builtin("++", 2, b_append),
92 builtin("startsWith", 2, b_starts_with),
92 builtin("startsWith", 2, b_starts_with),
@@ -95,14 +95,9 @@ pub fn all_builtins() -> Vec<(String, Value)> {
95 builtin("replay", 2, b_replay),
95 builtin("replay", 2, b_replay),
96 builtin("unresolved", 1, b_unresolved),
96 builtin("unresolved", 1, b_unresolved),
97 builtin("touchedPaths", 1, b_touched_paths),
97 builtin("touchedPaths", 1, b_touched_paths),
98− builtin("unrecorded", 1, b_unrecorded),
99− builtin("subtreeCommits", 1, b_subtree_commits),
100 builtin("blob", 1, b_blob),
98 builtin("blob", 1, b_blob),
101− builtin("blobHex", 2, b_blob_hex),
102 builtin("text", 1, b_text),
99 builtin("text", 1, b_text),
103 builtin("by", 2, b_by),
100 builtin("by", 2, b_by),
104− builtin("lookupCommit", 2, b_lookup_commit),
105− builtin("lookupContent", 2, b_lookup_content),
106 builtin("meta", 1, b_meta),
101 builtin("meta", 1, b_meta),
107 builtin("validate", 1, b_validate),
102 builtin("validate", 1, b_validate),
108 builtin("diff", 2, b_diff),
103 builtin("diff", 2, b_diff),
@@ -121,13 +116,33 @@ fn b_compose(_i: &mut Interp, args: &[Value]) -> BResult {
121 compose_values(_i, f, g)
116 compose_values(_i, f, g)
122}
117}
123
118
124−/// `map`, `filter`, `foldl` and a composition node apply the functions they
119+fn compose_apply(i: &mut Interp, args: &[Value]) -> BResult {
125−/// are given, so the evaluator runs them as steps of its machine
120+ let f = args[0].clone();
126−/// (Interp::step_applying): applying them in a nested run put every level of
121+ let g = args[1].clone();
127−/// a recursion through one on the native stack (§4.1). This only gives them a
122+ let x = args[2].clone();
128−/// name and an arity here; it is never called.
123+ // A composed function applied to a function does not run (the operand
124+ // contracts are about Repo-level values); it composes pointwise so that
125+ // definitions like `abandon . contract everything` and `tree . squash`
126+ // are functions waiting for the repository (§4.1 note, §1.2 step 7).
127+ if matches!(x, Value::Fun(_)) {
128+ let next = apply_or_compose(i, g, x)?;
129+ return apply_or_compose(i, f, next);
130+ }
131+ let gx = i.apply(g, x)?;
132+ i.apply(f, gx)
133+}
134+
135+/// Apply a signed function to an argument, composing instead when the
136+/// argument is a function but the contract's next parameter is not.
129−fn on_machine(_i: &mut Interp, _args: &[Value]) -> BResult {
137+pub fn apply_or_compose(i: &mut Interp, f: Value, x: Value) -> BResult {
130− Err(Crash::new("internal: a builtin that applies functions ran off the machine"))
138+ if matches!(x, Value::Fun(_)) {
139+ if let Some((_, cexpr)) = pending_of(&f) {
140+ if !cexpr.next_param_is_function(&i.shapes) {
141+ return compose_values(i, f, x);
142+ }
143+ }
144+ }
145+ i.apply(f, x)
131}
146}
132
147
133/// f . g as a value, with contract propagation
148/// f . g as a value, with contract propagation
@@ -139,8 +154,6 @@ pub fn compose_values(i: &mut Interp, f: Value, g: Value) -> BResult {
139 Box::new((*cf).clone()),
154 Box::new((*cf).clone()),
140 Box::new((*cg).clone()),
155 Box::new((*cg).clone()),
141 )),
156 )),
142− // named holding `f` and `g`
143− 2,
144 )),
157 )),
145 _ => None,
158 _ => None,
146 };
159 };
@@ -150,7 +163,7 @@ pub fn compose_values(i: &mut Interp, f: Value, g: Value) -> BResult {
150 name: "(.)".into(),
163 name: "(.)".into(),
151 arity: 3,
164 arity: 3,
152 args: vec![f, g],
165 args: vec![f, g],
153− f: on_machine,
166+ f: compose_apply,
154 pending,
167 pending,
155 })))
168 })))
156}
169}
@@ -169,9 +182,8 @@ pub fn pending_of(v: &Value) -> Option<(String, Rc<crate::shape::ContractExpr>)>
169 ..
182 ..
170 } => Some((n.clone(), Rc::new(crate::shape::ContractExpr::Unknown))),
183 } => Some((n.clone(), Rc::new(crate::shape::ContractExpr::Unknown))),
171 crate::value::FunVal::Builtin {
184 crate::value::FunVal::Builtin {
172− pending: Some((n, c, _)),
185+ pending: Some(p), ..
173− ..
174− } => Some((n.clone(), c.clone())),
186+ } => Some(p.clone()),
175 crate::value::FunVal::Builtin {
187 crate::value::FunVal::Builtin {
176 name, pending: None, ..
188 name, pending: None, ..
177 } => Some((name.clone(), Rc::new(crate::shape::ContractExpr::Unknown))),
189 } => Some((name.clone(), Rc::new(crate::shape::ContractExpr::Unknown))),
@@ -273,7 +285,7 @@ fn b_ge(_i: &mut Interp, args: &[Value]) -> BResult {
273}
285}
274
286
275fn b_show(i: &mut Interp, args: &[Value]) -> BResult {
287fn b_show(i: &mut Interp, args: &[Value]) -> BResult {
276− Ok(Value::text(crate::show::show(i, &args[0])?))
288+ Ok(Value::text(crate::show::show(i, &args[0])))
277}
289}
278
290
279fn b_cons(_i: &mut Interp, args: &[Value]) -> BResult {
291fn b_cons(_i: &mut Interp, args: &[Value]) -> BResult {
@@ -284,6 +296,35 @@ fn b_cons(_i: &mut Interp, args: &[Value]) -> BResult {
284 Ok(Value::list(v))
296 Ok(Value::list(v))
285}
297}
286
298
299+fn b_map(i: &mut Interp, args: &[Value]) -> BResult {
300+ want!(args, 0, Value::Fun(_), "a function");
301+ let xs = args[1].as_list()?;
302+ let mut out = Vec::with_capacity(xs.len());
303+ for x in xs.iter() {
304+ out.push(i.apply(args[0].clone(), x.clone())?);
305+ }
306+ Ok(Value::list(out))
307+}
308+
309+fn b_filter(i: &mut Interp, args: &[Value]) -> BResult {
310+ want!(args, 0, Value::Fun(_), "a function");
311+ let xs = args[1].as_list()?;
312+ let mut out = Vec::new();
313+ for x in xs.iter() {
314+ match i.apply(args[0].clone(), x.clone())? {
315+ Value::Bool(true) => out.push(x.clone()),
316+ Value::Bool(false) => (),
317+ v => {
318+ return Err(Crash::new(format!(
319+ "filter: predicate returned a {}",
320+ v.kind_name()
321+ )))
322+ }
323+ }
324+ }
325+ Ok(Value::list(out))
326+}
327+
287fn b_length(_i: &mut Interp, args: &[Value]) -> BResult {
328fn b_length(_i: &mut Interp, args: &[Value]) -> BResult {
288 Ok(Value::int(args[0].as_list()?.len() as i64))
329 Ok(Value::int(args[0].as_list()?.len() as i64))
289}
330}
@@ -380,6 +421,17 @@ fn b_range(_i: &mut Interp, args: &[Value]) -> BResult {
380 Ok(Value::list(out))
421 Ok(Value::list(out))
381}
422}
382
423
424+fn b_foldl(i: &mut Interp, args: &[Value]) -> BResult {
425+ want!(args, 0, Value::Fun(_), "a function");
426+ let xs = args[2].as_list()?;
427+ let mut acc = args[1].clone();
428+ for x in xs.iter() {
429+ let f = i.apply(args[0].clone(), acc)?;
430+ acc = i.apply(f, x.clone())?;
431+ }
432+ Ok(acc)
433+}
434+
383fn b_concat(_i: &mut Interp, args: &[Value]) -> BResult {
435fn b_concat(_i: &mut Interp, args: &[Value]) -> BResult {
384 let xs = args[0].as_list()?;
436 let xs = args[0].as_list()?;
385 if xs.is_empty() {
437 if xs.is_empty() {
@@ -457,11 +509,14 @@ fn b_split_on(_i: &mut Interp, args: &[Value]) -> BResult {
457// ----------------------------------------------------------------------
509// ----------------------------------------------------------------------
458
510
459fn b_replay(i: &mut Interp, args: &[Value]) -> BResult {
511fn b_replay(i: &mut Interp, args: &[Value]) -> BResult {
460− args[0].as_list()?;
512+ let onto = args[0].as_list()?;
461 let ch = &args[1];
513 let ch = &args[1];
462 let from = ch.field("from")?;
514 let from = ch.field("from")?;
463 let to = ch.field("to")?;
515 let to = ch.field("to")?;
516+ let from = from.as_list()?;
517+ let to = to.as_list()?;
464− i.backend.replay_snapshot(&args[0], &from, &to)
518+ let out = i.backend.replay(onto, from, to)?;
519+ Ok(Value::list(out))
465}
520}
466
521
467fn b_unresolved(_i: &mut Interp, args: &[Value]) -> BResult {
522fn b_unresolved(_i: &mut Interp, args: &[Value]) -> BResult {
@@ -498,53 +553,6 @@ fn b_touched_paths(_i: &mut Interp, args: &[Value]) -> BResult {
498 Ok(Value::list(out))
553 Ok(Value::list(out))
499}
554}
500
555
501−/// Working-directory edits captured at the start of this invocation, compared
502−/// with the recorded focus. Inspecting another commit or a hypothetical edit
503−/// must not change this answer. No disk scan or persistence happens here.
504−fn b_unrecorded(i: &mut Interp, _args: &[Value]) -> BResult {
505− let old = i.old_repo.borrow().clone();
506− let given = i.given_repo.borrow().clone();
507− let (Some(old), Some(given)) = (old, given) else {
508− // In-memory evaluations and reserved push expressions have no
509− // working-directory snapshot.
510− return Ok(Value::list(Vec::new()));
511− };
512− if matches!((&old, &given), (Value::Record(a), Value::Record(b)) if Rc::ptr_eq(a, b)) {
513− return Ok(Value::list(Vec::new()));
514− }
515− let from = old.field("root")?.field("files")?;
516− let to = given.field("root")?.field("files")?;
517− b_touched_paths(i, &[Value::record(&[
518− ("from", from),
519− ("to", to),
520− ])])
521−}
522−
523−/// every commit of a tree in preorder (§4.9): the value of the recursive
524−/// `\t -> t.root :: (concat (map subtreeCommits t.children) or [])`, which
525−/// the reference config's `commits` was. `::` and `concat` build new lists,
526−/// so that definition copied the list below every node and took time
527−/// quadratic in the length of the history, which the binary walks this way
528−/// on every run (`immutable`, §7.2). Its `or []` decides malformed trees:
529−/// only a top without `root` crashes, and a node whose `children` is not a
530−/// list of values with a `root` contributes its own root alone.
531−fn b_subtree_commits(_i: &mut Interp, args: &[Value]) -> BResult {
532− let mut out = Vec::new();
533− // the nodes still to visit, each with its root, the next one last
534− let mut todo = vec![(args[0].field("root")?, args[0].clone())];
535− while let Some((root, node)) = todo.pop() {
536− out.push(root);
537− let Ok(Value::List(kids)) = node.field("children") else {
538− continue;
539− };
540− let Ok(roots) = kids.iter().map(|k| k.field("root")).collect::<Result<Vec<_>, _>>() else {
541− continue;
542− };
543− todo.extend(roots.into_iter().zip(kids.iter().cloned()).rev());
544− }
545− Ok(Value::list(out))
546−}
547−
548fn b_blob(_i: &mut Interp, args: &[Value]) -> BResult {
556fn b_blob(_i: &mut Interp, args: &[Value]) -> BResult {
549 let t = args[0].as_text()?;
557 let t = args[0].as_text()?;
550 Ok(Value::Blob(Rc::new(BlobVal {
558 Ok(Value::Blob(Rc::new(BlobVal {
@@ -553,21 +561,6 @@ fn b_blob(_i: &mut Interp, args: &[Value]) -> BResult {
553 })))
561 })))
554}
562}
555
563
556−fn b_blob_hex(_i: &mut Interp, args: &[Value]) -> BResult {
557− let kind = match args[0].as_text()? {
558− "regular" => BlobKind::Regular,
559− "executable" => BlobKind::Executable,
560− "symlink" => BlobKind::Symlink,
561− _ => return Err(Crash::new("blobHex: kind must be regular, executable, or symlink")),
562− };
563− let bytes = hex::decode(args[1].as_text()?)
564− .map_err(|e| Crash::new(format!("blobHex: invalid hexadecimal content: {}", e)))?;
565− Ok(Value::Blob(Rc::new(BlobVal {
566− kind,
567− content: BlobContent::Resolved(Rc::new(bytes)),
568− })))
569−}
570−
571fn b_text(_i: &mut Interp, args: &[Value]) -> BResult {
564fn b_text(_i: &mut Interp, args: &[Value]) -> BResult {
572 match &args[0] {
565 match &args[0] {
573 Value::Blob(b) => {
566 Value::Blob(b) => {
@@ -601,18 +594,6 @@ fn b_by(_i: &mut Interp, args: &[Value]) -> BResult {
601 }
594 }
602}
595}
603
596
604−fn b_lookup_commit(_i: &mut Interp, args: &[Value]) -> BResult {
605− let id = match &args[0] {
606− Value::Id(id) => id.as_str(),
607− v => return Err(Crash::new(format!("by: expected an Id, got a {}", v.kind_name()))),
608− };
609− crate::repo::commit_by_id(&args[1], id)?.ok_or_else(|| Crash::new("by: no such commit"))
610−}
611−
612−fn b_lookup_content(_i: &mut Interp, args: &[Value]) -> BResult {
613− crate::lookup::content_at(&args[0], &args[1])
614−}
615−
616fn b_meta(i: &mut Interp, args: &[Value]) -> BResult {
597fn b_meta(i: &mut Interp, args: &[Value]) -> BResult {
617 let id = match &args[0] {
598 let id = match &args[0] {
618 Value::Id(id) => id.to_string(),
599 Value::Id(id) => id.to_string(),
@@ -634,11 +615,6 @@ fn b_meta(i: &mut Interp, args: &[Value]) -> BResult {
634
615
635fn b_validate(i: &mut Interp, args: &[Value]) -> BResult {
616fn b_validate(i: &mut Interp, args: &[Value]) -> BResult {
636 crate::repo::validate_repo(i, &args[0])?;
617 crate::repo::validate_repo(i, &args[0])?;
637− if let Some(loaded) = i.old_repo.borrow().as_ref() {
638− if !crate::repo::same_repo(loaded, &args[0])? {
639− i.backend.validate_authoring()?;
640− }
641− }
642 Ok(args[0].clone())
618 Ok(args[0].clone())
643}
619}
644
620
src/config.rs+54−370
@@ -39,8 +39,8 @@ const RESERVED_BUILTINS: &[&str] = &[
39 ".", "id", "const", "crash", "==", "/=", "&&", "||", "not", "+", "-", "*", "<", "<=", ">",
39 ".", "id", "const", "crash", "==", "/=", "&&", "||", "not", "+", "-", "*", "<", "<=", ">",
40 ">=", "show", "::", "map", "filter", "length", "null", "head", "tail", "last", "nth", "take",
40 ">=", "show", "::", "map", "filter", "length", "null", "head", "tail", "last", "nth", "take",
41 "drop", "member", "range", "foldl", "concat", "++", "startsWith", "endsWith", "splitOn",
41 "drop", "member", "range", "foldl", "concat", "++", "startsWith", "endsWith", "splitOn",
42− "replay", "unresolved", "blob", "blobHex", "text", "by", "meta", "validate", "diff", "difft", "treeWith",
42+ "replay", "unresolved", "blob", "text", "by", "meta", "validate", "diff", "difft", "treeWith",
43− "extract", "touchedPaths", "unrecorded", "subtreeCommits", "lookupCommit", "lookupContent",
43+ "extract", "touchedPaths",
44];
44];
45
45
46pub fn is_reserved_builtin(name: &str) -> bool {
46pub fn is_reserved_builtin(name: &str) -> bool {
@@ -53,8 +53,6 @@ pub fn reserved_set() -> BTreeSet<String> {
53
53
54/// Parse and validate config.j (§6.2). Id literals are resolved separately.
54/// Parse and validate config.j (§6.2). Id literals are resolved separately.
55pub fn load_config(src: &str) -> Result<Config, ConfigError> {
55pub fn load_config(src: &str) -> Result<Config, ConfigError> {
56− // the builtins only: config.j's binders may reuse its own top-level
57− // names (§4.2), as the reference config's `\files ->` does
58 let outer = Rc::new(reserved_set());
56 let outer = Rc::new(reserved_set());
59 let items = parse_config(src, outer).map_err(ConfigError::Parse)?;
57 let items = parse_config(src, outer).map_err(ConfigError::Parse)?;
60 validate_items(items)
58 validate_items(items)
@@ -97,24 +95,39 @@ fn validate_items(items: Vec<Item>) -> Result<Config, ConfigError> {
97 }
95 }
98
96
99 // pair signatures with definitions; a signature must immediately precede
97 // pair signatures with definitions; a signature must immediately precede
100− // its definition (blank lines/comments fine — items between are not, a
98+ // its definition (blank lines/comments fine — items between are not)
101− // typedecl or another signature included). A builtin's signature has no
102− // definition, so any item, or the end of the file, may follow it.
103− let mut prev_sig: Option<&String> = None;
99+ let mut prev_sig: Option<String> = None;
104− for item in items.iter().map(Some).chain([None]) {
100+ for item in &items {
105− if let Some(s) = prev_sig {
101+ match item {
102+ Item::Signature(name, _, _) => {
103+ prev_sig = Some(name.clone());
104+ }
106− let paired = matches!(item, Some(Item::Definition(name, _, _)) if name == s);
105+ Item::Definition(name, _, _) => {
106+ if let Some(s) = &prev_sig {
107+ if s != name {
108+ // signature not immediately followed by its definition
107− if !paired && def_names.contains(s) {
109+ if def_names.contains(s) {
108− return verr(format!(
110+ return verr(format!(
109− "the signature for `{}` is not immediately followed by its definition",
111+ "the signature for `{}` is not immediately followed by its definition",
110− s
112+ s
111− ));
113+ ));
114+ }
115+ }
116+ }
117+ prev_sig = None;
118+ }
119+ Item::TypeDecl(_, _) => {
120+ prev_sig = None;
112 }
121 }
113 }
122 }
123+ }
114− prev_sig = match item {
124+ if let Some(s) = &prev_sig {
115− Some(Item::Signature(name, _, _)) => Some(name),
125+ if def_names.contains(s) {
116− _ => None,
126+ return verr(format!(
127+ "the signature for `{}` is not immediately followed by its definition",
128+ s
117− };
129+ ));
130+ }
118 }
131 }
119
132
120 let mut builtin_names: HashSet<String> = HashSet::new();
133 let mut builtin_names: HashSet<String> = HashSet::new();
@@ -166,25 +179,7 @@ fn validate_items(items: Vec<Item>) -> Result<Config, ConfigError> {
166 })
179 })
167}
180}
168
181
169−/// Evaluation order of the definitions (§4.1).
182+/// topological order by references outside lambdas (§4.1)
170−///
171−/// References outside lambdas decide it, and only they can form a cycle;
172−/// ready definitions are taken in name order. That alone can apply a
173−/// function at load before a definition its body refers to (`opts = mk 1`
174−/// with `mk = \d -> { lanes = lanes }` and `lanes` still waiting), an unbound
175−/// name with no cycle anywhere. So the definitions are then grouped into the
176−/// strongly connected components of all references, lambda bodies included,
177−/// and the groups evaluated referenced first, the members of each in the
178−/// first order, save that a member which cannot apply a function of its group
179−/// at load goes first: one that refers to the group only inside lambdas it
180−/// stores (`env = { lanes = 3, header = \_ -> out }`, with `out = render 1`
181−/// and `render = \x -> env.lanes`). Such a member looks up only earlier
182−/// groups and builtins, and every other definition keeps its place among the
183−/// rest, so each still follows its references outside lambdas; and since
184−/// evaluating one looks up only names it reaches by references, whatever the
185−/// first order had bound in time still is (a lookup that came too early, its
186−/// crash caught by `or`, may now find its name). Among members that can apply
187−/// a function of their group, the first order may still apply one too early.
188fn dependency_order(
183fn dependency_order(
189 defs: &HashMap<String, Rc<Expr>>,
184 defs: &HashMap<String, Rc<Expr>>,
190) -> Result<Vec<(String, Rc<Expr>)>, ConfigError> {
185) -> Result<Vec<(String, Rc<Expr>)>, ConfigError> {
@@ -199,7 +194,7 @@ fn dependency_order(
199 let mut done: HashSet<String> = HashSet::new();
194 let mut done: HashSet<String> = HashSet::new();
200 let mut names: Vec<String> = defs.keys().cloned().collect();
195 let mut names: Vec<String> = defs.keys().cloned().collect();
201 names.sort();
196 names.sort();
202− while done.len() < names.len() {
197+ loop {
203 let mut progress = false;
198 let mut progress = false;
204 for n in &names {
199 for n in &names {
205 if done.contains(n) {
200 if done.contains(n) {
@@ -211,305 +206,15 @@ fn dependency_order(
211 progress = true;
206 progress = true;
212 }
207 }
213 }
208 }
209+ if done.len() == names.len() {
210+ return Ok(ordered);
211+ }
214 if !progress {
212 if !progress {
215 return Err(ConfigError::Validation(
213 return Err(ConfigError::Validation(
216 "config.j: a cycle among top-level definitions".into(),
214 "config.j: a cycle among top-level definitions".into(),
217 ));
215 ));
218 }
216 }
219 }
217 }
220− let position: HashMap<&str, usize> =
221− ordered.iter().enumerate().map(|(i, (n, _))| (n.as_str(), i)).collect();
222− let edges: Vec<Vec<usize>> = ordered
223− .iter()
224− .map(|(n, e)| {
225− let mut free = deps[n].clone();
226− e.free_vars(&mut Vec::new(), &mut free);
227− free.iter().filter_map(|n| position.get(n.as_str()).copied()).collect()
228− })
229− .collect();
230− let order = group_order(&edges, |group, p| {
231− let (n, e) = &ordered[p];
232− let mut eager = deps[n].clone();
233− applied_refs(e, true, &mut Vec::new(), &mut eager);
234− !eager
235− .iter()
236− .filter_map(|d| position.get(d.as_str()))
237− .any(|q| group.binary_search(q).is_ok())
238− });
239− Ok(order.into_iter().map(|p| ordered[p].clone()).collect())
240−}
241−
242−/// Positions in a first order regrouped for evaluation (§4.1): the strongly
243−/// connected components of `edges`, each after every one it has an edge
244−/// into, and inside a component of several, first the members for which
245−/// `early` holds, given the component (sorted) and the member, then the
246−/// others, each part in the first order.
247−fn group_order(edges: &[Vec<usize>], early: impl Fn(&[usize], usize) -> bool) -> Vec<usize> {
248− let mut out = Vec::with_capacity(edges.len());
249− for mut group in components(edges) {
250− group.sort_unstable();
251− if group.len() > 1 {
252− let (front, rest): (Vec<usize>, Vec<usize>) =
253− group.iter().partition(|&&p| early(&group, p));
254− group = front;
255− group.extend(rest);
256− }
257− out.extend(group);
258− }
259− out
260−}
261−
262−/// The strongly connected components of a graph given as adjacency lists,
263−/// each listed after every component it has an edge into (Tarjan's
264−/// algorithm, on an explicit stack so a long chain of definitions cannot
265−/// overflow the native one).
266−fn components(edges: &[Vec<usize>]) -> Vec<Vec<usize>> {
267− const UNSEEN: usize = usize::MAX;
268− let n = edges.len();
269− let (mut index, mut low, mut on_stack) = (vec![UNSEEN; n], vec![0; n], vec![false; n]);
270− let (mut stack, mut out, mut next) = (Vec::new(), Vec::new(), 0);
271− for root in 0..n {
272− if index[root] != UNSEEN {
273− continue;
274− }
275− // (vertex, how many of its edges have been followed)
276− let mut work = vec![(root, 0)];
277− while let Some(&(v, i)) = work.last() {
278− if i == 0 {
279− index[v] = next;
280− low[v] = next;
281− next += 1;
282− stack.push(v);
283− on_stack[v] = true;
284− }
285− if let Some(&w) = edges[v].get(i) {
286− work.last_mut().unwrap().1 = i + 1;
287− if index[w] == UNSEEN {
288− work.push((w, 0));
289− } else if on_stack[w] {
290− low[v] = low[v].min(index[w]);
291− }
292− continue;
293− }
294− work.pop();
295− if let Some(&(u, _)) = work.last() {
296− low[u] = low[u].min(low[v]);
297− }
298− if low[v] == index[v] {
299− let mut group = Vec::new();
300− loop {
301− let w = stack.pop().unwrap();
302− on_stack[w] = false;
303− group.push(w);
304− if w == v {
305− break;
306− }
307− }
308− out.push(group);
309− }
310− }
311− }
312− out
313−}
314−
315−pub(crate) struct LetPlan {
316− pub order: Vec<usize>,
317− /// Whether a closure needs to see its own or a later binding. Otherwise
318− /// completed bindings can be held in immutable, acyclic environments.
319− pub recursive: bool,
320−}
321−
322−/// Plan the bindings of a `let` block (§4.1), or return `None` when references
323−/// outside lambdas form a cycle.
324−///
325−/// The rule is `dependency_order`'s, with ready bindings taken in source
326−/// order: a binding follows every binding it refers to outside a lambda; the
327−/// bindings are then grouped by all their references, and inside a group
328−/// those that cannot apply a function of it go first, so that a function
329−/// applied in a binding finds the bindings its body refers to. That order is
330−/// the source order when every reference is to an earlier binding, or from
331−/// inside a lambda to its own, as in nearly every block, which is so taken
332−/// without the sorting: this runs each time a block is evaluated.
333−pub(crate) fn let_plan(bs: &[(String, Rc<Expr>)]) -> Option<LetPlan> {
334− let refs: Vec<Vec<(usize, bool)>> = bs
335− .iter()
336− .map(|(_, e)| {
337− let mut out = Vec::new();
338− binding_refs(e, bs, false, &mut out);
339− out
340− })
341− .collect();
342− let order = let_order(bs, &refs)?;
343− let mut position = vec![0; bs.len()];
344− for (p, &i) in order.iter().enumerate() {
345− position[i] = p;
346− }
347− let recursive = refs.iter().enumerate().any(|(i, refs)| {
348− refs.iter().any(|&(j, _)| position[j] >= position[i])
349− });
350− Some(LetPlan { order, recursive })
351−}
352−
353−fn let_order(bs: &[(String, Rc<Expr>)], refs: &[Vec<(usize, bool)>]) -> Option<Vec<usize>> {
354− let in_order =
355− |i: usize, r: &[(usize, bool)]| r.iter().all(|&(j, outside)| j < i || (j == i && !outside));
356− if refs.iter().enumerate().all(|(i, r)| in_order(i, r)) {
357− return Some((0..bs.len()).collect());
358− }
359− let mut ordered = Vec::with_capacity(bs.len());
360− let mut done = vec![false; bs.len()];
361− while ordered.len() < bs.len() {
362− let mut progress = false;
363− for i in 0..bs.len() {
364− if !done[i] && refs[i].iter().all(|&(j, outside)| !outside || done[j]) {
365− done[i] = true;
366− ordered.push(i);
367− progress = true;
368− }
369− }
370− if !progress {
371− return None;
372− }
373− }
374− let mut position = vec![0; bs.len()];
375− for (p, &i) in ordered.iter().enumerate() {
376− position[i] = p;
377− }
378− let edges: Vec<Vec<usize>> = ordered
379− .iter()
380− .map(|&i| refs[i].iter().map(|&(j, _)| position[j]).collect())
381− .collect();
382− let order = group_order(&edges, |group, p| {
383− let i = ordered[p];
384− let mut applied = BTreeSet::new();
385− applied_refs(&bs[i].1, true, &mut Vec::new(), &mut applied);
386− let in_group = |j: usize| group.binary_search(&position[j]).is_ok();
387− !refs[i].iter().any(|&(j, outside)| outside && in_group(j))
388− && !bs.iter().enumerate().any(|(j, (b, _))| applied.contains(b) && in_group(j))
389− });
390− Some(order.into_iter().map(|p| ordered[p]).collect())
391−}
392−
393−/// Record the references in `e` to the names a `let` block binds, as indices
394−/// into its bindings `bs`, each with whether it lies outside every lambda,
395−/// where evaluating `e` needs that binding's value (§4.1). No binder inside
396−/// the block may reuse one of its names (§4.2), so every occurrence is one.
397−fn binding_refs(
398− e: &Expr,
399− bs: &[(String, Rc<Expr>)],
400− in_lambda: bool,
401− out: &mut Vec<(usize, bool)>,
402−) {
403− let mut sub = |e: &Expr| binding_refs(e, bs, in_lambda, out);
404− match e {
405− Expr::Var(n) => {
406− if let Some(j) = bs.iter().position(|(b, _)| b == n) {
407− out.push((j, !in_lambda));
408− }
409− }
410− // `%main` is `labelled "main"` (§4.11), and in config.j a block may
411− // bind `labelled` (§4.2)
412− Expr::LabelLit(_) => {
413− if let Some(j) = bs.iter().position(|(b, _)| b == "labelled") {
414− out.push((j, !in_lambda));
415− }
416− }
417− Expr::Lambda(_, body, _) => binding_refs(body, bs, true, out),
418− Expr::If(a, b, c) => {
419− sub(a);
420− sub(b);
421− sub(c);
422− }
423− Expr::Let(inner, body) => {
424− for (_, e) in inner {
425− sub(e);
426− }
427− sub(body);
428− }
429− Expr::App(a, b) | Expr::BinOp(_, a, b) | Expr::Or(a, b) => {
430− sub(a);
431− sub(b);
432− }
433− Expr::Select(a, _) | Expr::Paren(a) => sub(a),
434− Expr::Update(a, fs) => {
435− sub(a);
436− for (_, v) in fs {
437− sub(v);
438− }
439− }
440− Expr::Record(fs) => {
441− for (_, v) in fs {
442− sub(v);
443− }
444− }
445− Expr::List(es) => {
446− for e in es {
447− sub(e);
448− }
449− }
450− Expr::TypeName(_)
451− | Expr::Int(_)
452− | Expr::Text(_)
453− | Expr::IdLit(_)
454− | Expr::Id(_)
455− | Expr::NewId
456− | Expr::PathLit(_)
457− | Expr::Bool(_)
458− | Expr::SelectorFun(_)
459− | Expr::Crash => {}
460− }
461−}
462−
463−/// Record the free names of the lambdas that evaluating `e` may apply
464−/// (§4.1): every lambda outside the others, save where its value is only
465−/// stored, which is `e` itself when `stored`, and a record field, list
466−/// element, `if` branch or `let` body of such a place. `bound` holds the
467−/// `let` names in scope, which in config.j may reuse a top-level name (§4.2).
468−fn applied_refs(e: &Expr, stored: bool, bound: &mut Vec<String>, out: &mut BTreeSet<String>) {
469− match e {
470− Expr::Lambda(_, _, _) => {
471− if !stored {
472− e.free_vars(bound, out);
473− }
474− }
475− Expr::Paren(a) => applied_refs(a, stored, bound, out),
476− Expr::If(c, a, b) => {
477− applied_refs(c, false, bound, out);
478− applied_refs(a, stored, bound, out);
479− applied_refs(b, stored, bound, out);
480− }
481− Expr::Let(bs, body) => {
482− let start = bound.len();
483− bound.extend(bs.iter().map(|(n, _)| n.clone()));
484− for (_, b) in bs {
485− applied_refs(b, false, bound, out);
486− }
487− applied_refs(body, stored, bound, out);
488− bound.truncate(start);
489− }
490− Expr::App(a, b) | Expr::BinOp(_, a, b) | Expr::Or(a, b) => {
491− applied_refs(a, false, bound, out);
492− applied_refs(b, false, bound, out);
493− }
494− Expr::Select(a, _) => applied_refs(a, false, bound, out),
495− Expr::Update(a, fs) => {
496− applied_refs(a, false, bound, out);
497− for (_, v) in fs {
498− applied_refs(v, stored, bound, out);
499− }
500− }
501− Expr::Record(fs) => {
502− for (_, v) in fs {
503− applied_refs(v, stored, bound, out);
504− }
505− }
506− Expr::List(es) => {
507− for e in es {
508− applied_refs(e, stored, bound, out);
509− }
510− }
511− _ => {}
512− }
513}
218}
514
219
515/// Names a definition's value depends on at load time (§4.1). Lambda bodies
220/// Names a definition's value depends on at load time (§4.1). Lambda bodies
@@ -571,10 +276,6 @@ fn load_deps(e: &Expr, out: &mut BTreeSet<String>) {
571 load_deps(e, out);
276 load_deps(e, out);
572 }
277 }
573 }
278 }
574− // `%main` is `labelled "main"` (§4.11), evaluated where it stands
575− Expr::LabelLit(_) => {
576− out.insert("labelled".to_string());
577− }
578 _ => {}
279 _ => {}
579 }
280 }
580}
281}
@@ -603,34 +304,29 @@ pub fn eval_config(interp: &mut Interp, cfg: &Config) -> Result<(), Crash> {
603 }
304 }
604 }
305 }
605 interp.globals = Env::with_globals(globals);
306 interp.globals = Env::with_globals(globals);
606− // a definition executes while its lambda's body runs (§1.4)
607− for (name, expr) in &cfg.defs {
608− interp.name_bodies(name, expr);
609− }
610 // evaluate definitions in dependency order, through one recursive frame so
307 // evaluate definitions in dependency order, through one recursive frame so
611 // top-level definitions are mutually recursive (§4.1)
308 // top-level definitions are mutually recursive (§4.1)
612− let names: Rc<[String]> = cfg.defs.iter().map(|(name, _)| name.clone()).collect();
613− let (genv, cell) = interp.globals.extend_rec(names);
309+ let (genv, cell) = interp.globals.extend_rec();
614 for (name, expr) in &cfg.defs {
310 for (name, expr) in &cfg.defs {
615 *interp.current_def.borrow_mut() = Some(name.clone());
311 *interp.current_def.borrow_mut() = Some(name.clone());
616 let v = interp.eval(expr, &genv)?;
312 let v = interp.eval(expr, &genv)?;
617− // the value itself, one level deep, against the whole signature
313+ // value-level contract for non-function definitions
618− // (§4.13): under a function type, including an alias of one, it must
619− // be a function, however it was built (`conflicts : Revset` is a
620− // partial application, `myEdit : Edit` with `myEdit = 5` is refused)
621 if let Some(ty) = cfg.sigs.get(name) {
314 if let Some(ty) = cfg.sigs.get(name) {
315+ let c = compile_contract(&interp.shapes, ty);
316+ if c.params.is_empty() {
622− if let Err(msg) = crate::shape::check(&interp.shapes, ty, &v) {
317+ if let Err(msg) = crate::shape::check(&interp.shapes, &c.result, &v) {
623− return Err(Crash::new(format!("contract: {}: {}", name, msg)));
318+ return Err(Crash::new(format!("contract: {}: {}", name, msg)));
319+ }
624 }
320 }
625 }
321 }
626− cell.borrow_mut().push(v.clone());
322+ cell.borrow_mut().push((name.clone(), v.clone()));
627 // attach the definition's name and contract to its function value so
323 // attach the definition's name and contract to its function value so
628 // applications are checked (§4.13) and errors name the definition
324 // applications are checked (§4.13) and errors name the definition
629 if let Some(ty) = cfg.sigs.get(name) {
325 if let Some(ty) = cfg.sigs.get(name) {
630 let contract = Rc::new(compile_contract(&interp.shapes, ty));
326 let contract = Rc::new(compile_contract(&interp.shapes, ty));
631 let named = crate::value::attach_pending(&v, name, contract);
327 let named = crate::value::attach_pending(&v, name, contract);
632 let last = cell.borrow_mut().len() - 1;
328 let last = cell.borrow_mut().len() - 1;
633− cell.borrow_mut()[last] = named;
329+ cell.borrow_mut()[last] = (name.clone(), named);
634 }
330 }
635 }
331 }
636 interp.globals = genv;
332 interp.globals = genv;
@@ -768,29 +464,17 @@ fn resolve_ids_rec(
768 }
464 }
769}
465}
770
466
771−/// Resolve every `@prefix` literal in the config's definitions against the
467+/// find Id literals per definition for config validation (§6.2.5)
772−/// visible ids, in place (§1.2.5: "in `config.j`, every literal is resolved at
468+pub fn config_id_failures(
773−/// load"). Returns the ones that did not resolve to exactly one commit, as
774−/// (definition, prefix, candidates); those definitions are left untouched.
775−///
776−/// Checking without rewriting is not enough: an unresolved literal reaching
777−/// the evaluator crashes with `internal: unresolved id literal`, and because a
778−/// lambda body is not evaluated at load, it does so only when the definition
779−/// is eventually used.
780−pub fn resolve_config_ids(
781− cfg: &mut Config,
469+ cfg: &Config,
782 interp: &Interp,
470 interp: &Interp,
783) -> Vec<(String, String, Vec<String>)> {
471) -> Vec<(String, String, Vec<String>)> {
784 let mut out = Vec::new();
472 let mut out = Vec::new();
785− for (name, expr) in cfg.defs.iter_mut() {
473+ for (name, expr) in &cfg.defs {
786 let mut failures = Vec::new();
474 let mut failures = Vec::new();
787− let resolved = resolve_ids_rec(expr, interp, &mut failures);
475+ resolve_ids_rec(expr, interp, &mut failures);
788− if failures.is_empty() {
789− *expr = Rc::new(resolved);
790− } else {
791− for (prefix, matches) in failures {
476+ for (prefix, matches) in failures {
792− out.push((name.clone(), prefix, matches));
477+ out.push((name.clone(), prefix, matches));
793− }
794 }
478 }
795 }
479 }
796 out
480 out
src/diagnostics.rsdeleted−381
@@ -1,381 +0,0 @@
1−//! Read-only guidance for common CLI mistakes. The original error remains the
2−//! authority: these helpers neither retry an expression nor change its meaning.
3−
4−use crate::ast::{Expr, TypeExpr};
5−use crate::config::Config;
6−use crate::parse::parse_expr;
7−use crate::shape::{compile_contract, type_name};
8−use crate::value::Crash;
9−use std::collections::BTreeSet;
10−use std::rc::Rc;
11−use unicode_width::UnicodeWidthChar;
12−
13−/// Add a location and the offending source line without inventing a column
14−/// that the parser does not currently retain.
15−pub fn format_source_error(label: &str, src: &str, line: usize, msg: &str) -> String {
16− let mut output = format!("{}: line {}: {}", label, line, msg);
17− if let Some(source_line) = line.checked_sub(1).and_then(|n| src.split('\n').nth(n)) {
18− output.push_str(&format!("\n {} | {}", line, source_excerpt(source_line.trim_end_matches('\r'))));
19− }
20− output
21−}
22−
23−/// Leave room for an ellipsis, count wide characters, and render terminal
24−/// controls as visible escapes. Charge even zero-width characters one column
25−/// so a malformed line of combining marks cannot produce unlimited output.
26−fn source_excerpt(line: &str) -> String {
27− const MAX_COLUMNS: usize = 160;
28− let mut excerpt = String::new();
29− let mut columns = 0;
30− for character in line.chars() {
31− let visible = if character.is_control() {
32− character.escape_default().to_string()
33− } else {
34− character.to_string()
35− };
36− let width: usize = visible.chars().map(|c| c.width().unwrap_or(0).max(1)).sum();
37− if columns + width > MAX_COLUMNS - 1 {
38− excerpt.push('…');
39− break;
40− }
41− excerpt.push_str(&visible);
42− columns += width;
43− }
44− excerpt
45−}
46−
47−/// Suggest a next step only when the reported failure agrees with the user's
48−/// expression. In particular, an error inside a config definition must not be
49−/// misdiagnosed as a missing argument on the command line.
50−pub fn expression_hint(text: &str, cfg: &Config, crash: &Crash) -> Option<String> {
51− expression_hint_for(text, cfg, crash, Invocation::Expression)
52−}
53−
54−/// Diagnose the expression following `push`, preserving that reserved command
55−/// in copyable examples so following a hint cannot turn a push into a local edit.
56−pub fn push_expression_hint(text: &str, cfg: &Config, crash: &Crash) -> Option<String> {
57− expression_hint_for(text, cfg, crash, Invocation::Push)
58−}
59−
60−#[derive(Clone, Copy)]
61−enum Invocation {
62− Expression,
63− Push,
64−}
65−
66−fn expression_hint_for(text: &str, cfg: &Config, crash: &Crash, invocation: Invocation) -> Option<String> {
67− let expr = parse_expr(text, Rc::new(cfg.global_names.clone())).ok()?;
68− let names = vocabulary(cfg);
69− if let Some(name) = crash.msg.strip_prefix("unbound name `").and_then(|s| s.strip_suffix('`')) {
70− if crash.def.is_some() {
71− return None;
72− }
73− let mut free = BTreeSet::new();
74− expr.free_vars(&mut Vec::new(), &mut free);
75− if !free.contains(name) {
76− return None;
77− }
78− if let Some(hint) = text_argument_hint(&expr, cfg, &names, name, invocation) {
79− return Some(hint);
80− }
81− let mut suggestions: Vec<_> = names
82− .iter()
83− .filter(|candidate| candidate.chars().next().is_some_and(|c| c.is_alphabetic() || c == '_'))
84− .filter_map(|candidate| {
85− let limit = if name.chars().count() > 4 { 2 } else { 1 };
86− let distance = edit_distance(name, candidate, limit)?;
87− (distance > 0).then_some((distance, *candidate))
88− })
89− .collect();
90− suggestions.sort_unstable();
91− if let Some((distance, best)) = suggestions.first().copied() {
92− let close: Vec<_> = suggestions
93− .iter()
94− .take_while(|(d, _)| *d == distance)
95− .take(3)
96− .map(|(_, n)| format!("`{}`", n))
97− .collect();
98− return Some(if close.len() == 1 {
99− format!("Did you mean `{}`? See `{}`.", best, help_command(best))
100− } else {
101− format!("Did you mean {}? Use `j help NAME` for details.", close.join(" or "))
102− });
103− }
104− return Some("Run `j commands` to browse the names available in your config.".into());
105− }
106−
107− let (name, args) = application(&expr)?;
108− let signature = cfg.sigs.get(name)?;
109− let contract = compile_contract(&cfg.shapes, signature);
110− let parameter = contract.params.get(args.len())?;
111− // A record supplied explicitly fails at an earlier argument. Requiring the
112− // next argument to match confines this hint to the implicit repo argument.
113− if !crash.msg.starts_with(&format!("contract: {} expected ", name))
114− || !crash.msg.ends_with(&format!(" as argument {}, got record", args.len() + 1))
115− || matches!(cfg.shapes.unfold(parameter), TypeExpr::Record(_))
116− {
117− return None;
118− }
119− let expected = type_name(parameter);
120− Some(format!(
121− "`{}` needs an argument of type {} before it can receive the repository. See `{}`.",
122− name, expected, help_command(name)
123− ))
124−}
125−
126−/// `global_names` also reserves every implemented builtin for shadowing checks.
127−/// Only names actually supplied by this config are useful typo suggestions.
128−fn vocabulary(cfg: &Config) -> BTreeSet<&str> {
129− cfg.defs
130− .iter()
131− .map(|(name, _)| name.as_str())
132− .chain(cfg.builtin_names.iter().map(String::as_str))
133− .collect()
134−}
135−
136−fn unparen(mut expr: &Expr) -> &Expr {
137− while let Expr::Paren(inner) = expr {
138− expr = inner;
139− }
140− expr
141−}
142−
143−fn application(expr: &Expr) -> Option<(&str, Vec<&Expr>)> {
144− let mut head = unparen(expr);
145− let mut args = Vec::new();
146− while let Expr::App(function, argument) = head {
147− args.push(unparen(argument));
148− head = unparen(function);
149− }
150− args.reverse();
151− match head {
152− Expr::Var(name) => Some((name, args)),
153− _ => None,
154− }
155−}
156−
157−fn text_argument_hint(
158− expr: &Expr,
159− cfg: &Config,
160− names: &BTreeSet<&str>,
161− missing: &str,
162− invocation: Invocation,
163−) -> Option<String> {
164− let (name, args) = application(expr)?;
165− let contract = compile_contract(&cfg.shapes, cfg.sigs.get(name)?);
166− let index = args.iter().position(|arg| matches!(arg, Expr::Var(n) if n == missing))?;
167− let parameter = contract.params.get(index)?;
168− if !matches!(cfg.shapes.unfold(parameter), TypeExpr::Con(n) if n == "Text") {
169− return None;
170− }
171−
172− // For a single Text argument, shell-stripped words can be shown in a
173− // complete, copyable example. Do not guess how to group multiple Text
174− // parameters or how to rewrite a larger expression.
175− let only_text_argument = index == 0
176− && contract.params.iter().skip(1).all(|ty| matches!(ty.as_ref(), TypeExpr::Con(n) if n == "Repo"));
177− let words: Option<Vec<_>> = args
178− .iter()
179− .map(|arg| match arg {
180− Expr::Var(n) if !names.contains(n.as_str()) => Some(n.as_str()),
181− _ => None,
182− })
183− .collect();
184− let prefix = match invocation {
185− Invocation::Expression => "",
186− Invocation::Push => "push ",
187− };
188− match words.filter(|_| only_text_argument) {
189− Some(words) => Some(format!(
190− "If you meant text, preserve its double quotes by quoting the whole expression: j {}. See `{}`.",
191− shell_quote(&format!("{}{} \"{}\"", prefix, name, words.join(" "))), help_command(name)
192− )),
193− None => Some(format!(
194− "If you meant text, use double quotes for the text inside shell single quotes around the expression. See `{}`.",
195− help_command(name)
196− )),
197− }
198−}
199−
200−fn shell_quote(text: &str) -> String {
201− format!("'{}'", text.replace('\'', "'\\''"))
202−}
203−
204−fn help_command(name: &str) -> String {
205− let argument = if name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
206− name.to_string()
207− } else {
208− shell_quote(name)
209− };
210− format!("j help {}", argument)
211−}
212−
213−/// Bounded Damerau-Levenshtein distance catches adjacent transpositions as well
214−/// as inserted, removed, and replaced letters. Long identifiers are skipped so
215−/// error reporting cannot become expensive for generated input.
216−fn edit_distance(a: &str, b: &str, limit: usize) -> Option<usize> {
217− let a: Vec<_> = a.chars().take(129).collect();
218− let b: Vec<_> = b.chars().take(129).collect();
219− if a.len() > 128 || b.len() > 128 || a.len().abs_diff(b.len()) > limit {
220− return None;
221− }
222− let mut older = vec![0; b.len() + 1];
223− let mut previous: Vec<_> = (0..=b.len()).collect();
224− let mut current = vec![0; b.len() + 1];
225− for (i, letter) in a.iter().enumerate() {
226− current[0] = i + 1;
227− for (k, other) in b.iter().enumerate() {
228− current[k + 1] = (previous[k + 1] + 1)
229− .min(current[k] + 1)
230− .min(previous[k] + usize::from(letter != other));
231− if i > 0 && k > 0 && *letter == b[k - 1] && a[i - 1] == *other {
232− current[k + 1] = current[k + 1].min(older[k - 1] + 1);
233− }
234− }
235− std::mem::swap(&mut older, &mut previous);
236− std::mem::swap(&mut previous, &mut current);
237− }
238− (previous[b.len()] <= limit).then_some(previous[b.len()])
239−}
240−
241−#[cfg(test)]
242−mod tests {
243− use super::*;
244− use crate::config::{eval_config, load_config};
245− use crate::eval::Interp;
246− use crate::value::Value;
247−
248− fn reference() -> Config {
249− load_config(include_str!("../config.j")).unwrap()
250− }
251−
252− fn failure(cfg: &Config, text: &str) -> Crash {
253− let mut interp = Interp::dummy();
254− interp.shapes = cfg.shapes.clone();
255− eval_config(&mut interp, cfg).unwrap();
256− let expr = Rc::new(parse_expr(text, Rc::new(cfg.global_names.clone())).unwrap());
257− let result = interp.eval(&expr, &interp.global_env()).and_then(|value| match value {
258− Value::Fun(_) => interp.apply(value, Value::record(&[])),
259− value => Ok(value),
260− });
261− result.expect_err("test expression must fail")
262− }
263−
264− #[test]
265− fn suggests_real_commands_and_transposed_names() {
266− let cfg = reference();
267− for (expression, expected) in [("neww", "j help new"), ("desrcibe", "j help describe")] {
268− let hint = expression_hint(expression, &cfg, &failure(&cfg, expression)).unwrap();
269− assert!(hint.contains(expected), "{}", hint);
270− }
271− }
272−
273− #[test]
274− fn suggests_custom_definitions_but_not_undeclared_builtins() {
275− let cfg = load_config("user = { name = \"A\", email = \"a@b\" }\ntree = \\r -> \"\"\nimmutable = \\r -> []\nlabelled = \\n r -> []\nship = \\r -> r\n").unwrap();
276− let hint = expression_hint("shipp", &cfg, &failure(&cfg, "shipp")).unwrap();
277− assert!(hint.contains("j help ship"));
278− let hint = expression_hint("repla", &cfg, &failure(&cfg, "repla")).unwrap();
279− assert!(!hint.contains("replay"));
280− assert!(hint.contains("j commands"));
281− }
282−
283− #[test]
284− fn explains_shell_quoting_only_for_text_parameters() {
285− let cfg = reference();
286− for expression in ["describe hello", "describe first change"] {
287− let hint = expression_hint(expression, &cfg, &failure(&cfg, expression)).unwrap();
288− assert!(hint.contains("j 'describe \""), "{}", hint);
289− assert!(hint.contains("j help describe"));
290− }
291− let hint = expression_hint("rebase helloo", &cfg, &failure(&cfg, "rebase helloo")).unwrap();
292− assert!(!hint.contains("quotes"));
293− }
294−
295− #[test]
296− fn push_quoting_examples_preserve_the_reserved_command() {
297− let cfg = reference();
298− for (expression, corrected) in [
299− ("unlabel feature", "unlabel \"feature\""),
300− ("describe hello", "describe \"hello\""),
301− ] {
302− let crash = failure(&cfg, expression);
303− let hint = push_expression_hint(expression, &cfg, &crash).unwrap();
304− assert!(hint.contains(&format!("j 'push {}'", corrected)), "{}", hint);
305− assert!(!hint.contains(&format!("j '{}'", corrected)), "{}", hint);
306−
307− let ordinary = expression_hint(expression, &cfg, &crash).unwrap();
308− assert!(ordinary.contains(&format!("j '{}'", corrected)), "{}", ordinary);
309− assert!(!ordinary.contains("j 'push "), "{}", ordinary);
310− }
311− }
312−
313− #[test]
314− fn explains_implicit_repo_application_but_not_explicit_type_errors() {
315− let cfg = reference();
316− for expression in ["describe", "rebase", "split"] {
317− let hint = expression_hint(expression, &cfg, &failure(&cfg, expression)).unwrap();
318− assert!(hint.contains("before it can receive the repository"), "{}", hint);
319− assert!(hint.contains(&format!("j help {}", expression)));
320− }
321− assert!(expression_hint("describe {}", &cfg, &failure(&cfg, "describe {}")).is_none());
322− assert!(expression_hint("describe 42", &cfg, &failure(&cfg, "describe 42")).is_none());
323− }
324−
325− #[test]
326− fn custom_signatures_drive_hints_after_partial_application() {
327− let src = format!("{}\ncustom : Text -> Int -> Edit\ncustom = \\message count repo -> repo\n", include_str!("../config.j"));
328− let cfg = load_config(&src).unwrap();
329− let expression = "custom \"message\"";
330− let hint = expression_hint(expression, &cfg, &failure(&cfg, expression)).unwrap();
331− assert!(hint.contains("needs an argument of type Int"), "{}", hint);
332− assert!(hint.contains("j help custom"));
333− let expression = "custom \"message\" {}";
334− assert!(expression_hint(expression, &cfg, &failure(&cfg, expression)).is_none());
335− }
336−
337− #[test]
338− fn quote_examples_preserve_apostrophes_in_valid_identifiers() {
339− let cfg = reference();
340− let expression = "describe don't";
341− let hint = expression_hint(expression, &cfg, &failure(&cfg, expression)).unwrap();
342− assert!(hint.contains("j 'describe \"don'\\''t\"'"), "{}", hint);
343− }
344−
345− #[test]
346− fn does_not_blame_the_cli_for_unbound_names_inside_definitions() {
347− let cfg = reference();
348− assert!(expression_hint("new", &cfg, &Crash::new("unbound name `neww`")).is_none());
349− assert!(expression_hint("\\x -> x", &cfg, &Crash::new("unbound name `x`")).is_none());
350− }
351−
352− #[test]
353− fn source_errors_name_the_file_and_preserve_line_numbers() {
354− let output = format_source_error("/tmp/config.j", "first\nbroken =\n", 2, "unexpected newline");
355− assert_eq!(output, "/tmp/config.j: line 2: unexpected newline\n 2 | broken =");
356− assert_eq!(format_source_error("expression", "bad\n", 2, "unexpected end"), "expression: line 2: unexpected end\n 2 | ");
357− assert_eq!(format_source_error("expression", "bad", 8, "unexpected end"), "expression: line 8: unexpected end");
358− }
359−
360− #[test]
361− fn huge_unicode_source_lines_have_bounded_excerpts() {
362− use unicode_width::UnicodeWidthStr;
363−
364− for source in ["(".repeat(200_000), "界é🙂\u{301}".repeat(50_000), "\u{301}".repeat(200_000)] {
365− let output = format_source_error("expression", &source, 1, "nested too deeply");
366− let excerpt = output.split_once(" 1 | ").unwrap().1;
367− assert!(excerpt.ends_with('…'));
368− assert!(excerpt.width() <= 160, "excerpt exceeds display width budget");
369− assert!(excerpt.chars().count() <= 160, "zero-width characters must also be bounded");
370− assert!(output.len() < 700, "diagnostic must not repeat the entire huge line");
371− }
372− }
373−
374− #[test]
375− fn source_excerpts_escape_terminal_controls() {
376− let output = format_source_error("expression", "\tfirst\u{1b}[31m\rsecond\u{7}", 1, "invalid input");
377− let excerpt = output.split_once(" 1 | ").unwrap().1;
378− assert_eq!(excerpt, "\\tfirst\\u{1b}[31m\\rsecond\\u{7}");
379− assert!(!excerpt.chars().any(char::is_control));
380− }
381−}
src/domain.rs+19−154
@@ -2,7 +2,7 @@
2//! (§7, §10). Two implementations: an in-memory backend for tests, and the
2//! (§7, §10). Two implementations: an in-memory backend for tests, and the
3//! jj-lib backend.
3//! jj-lib backend.
4
4
5−use crate::value::{BlobContent, BlobKind, BlobVal, ConflictSide, Crash, Value};
5+use crate::value::{BlobContent, BlobKind, BlobVal, Crash, Value};
6use std::collections::{BTreeMap, BTreeSet, HashMap};
6use std::collections::{BTreeMap, BTreeSet, HashMap};
7use std::rc::Rc;
7use std::rc::Rc;
8
8
@@ -29,12 +29,6 @@ pub struct ImmutabilityInfo {
29}
29}
30
30
31pub trait Backend {
31pub trait Backend {
32− /// Refuse an edit that cannot be authored with the backend's current
33− /// configuration. In-memory repositories have no authoring requirement.
34− fn validate_authoring(&self) -> Result<(), Crash> {
35− Ok(())
36− }
37−
38 /// All visible change ids (full), for resolving `@prefix` literals.
32 /// All visible change ids (full), for resolving `@prefix` literals.
39 fn visible_ids(&self) -> Vec<String>;
33 fn visible_ids(&self) -> Vec<String>;
40 /// All visible ids beginning with `prefix`.
34 /// All visible ids beginning with `prefix`.
@@ -48,38 +42,19 @@ pub trait Backend {
48 fn meta(&self, id: &str) -> Result<MetaInfo, Crash>;
42 fn meta(&self, id: &str) -> Result<MetaInfo, Crash>;
49 /// True if the change id denotes a merge commit (2+ parents).
43 /// True if the change id denotes a merge commit (2+ parents).
50 fn is_merge(&self, id: &str) -> bool;
44 fn is_merge(&self, id: &str) -> bool;
51− /// True if the stored commit's tree contains a conflict, if the backend
45+ /// True if the commit's tree contains a conflict, if the backend can say
52− /// can say without reading files (jj: O(1)); None means "unknown, compare
46+ /// without reading files (jj: O(1)); None means "unknown, compare files".
53− /// files". It describes a commit value only while that value still holds
54− /// the stored files (`ThunkVal::origin`), which is when the renderer asks.
55 fn has_conflict(&self, _id: &str) -> Option<bool> {
47 fn has_conflict(&self, _id: &str) -> Option<bool> {
56 None
48 None
57 }
49 }
58− /// True if the stored commit `id`'s tree equals the stored commit
50+ /// True if the commit's tree equals its first parent's tree, if the
59− /// `parent`'s, if the backend can say without reading files; None means
51+ /// backend can say without reading files; None means "compare files".
60− /// "compare files". Asked, like `has_conflict`, only for values that still
61− /// hold both stored trees.
62− fn is_empty(&self, _id: &str, _parent: &str) -> Option<bool> {
52+ fn is_empty(&self, _id: &str) -> Option<bool> {
63 None
53 None
64 }
54 }
65− /// All ancestors (inclusive) of the given change ids, through every
55+ /// All ancestors (inclusive) of the given change ids.
66− /// parent of a merge, not only the first (§7.5 step 3).
67 fn ancestors_closed(&self, ids: &BTreeSet<String>) -> BTreeSet<String>;
56 fn ancestors_closed(&self, ids: &BTreeSet<String>) -> BTreeSet<String>;
68
57
69− /// True if the working directory's filesystem folds case, so `.GIT`
70− /// names `.git` and jj's checkout refuses it (§7.5 step 1).
71− fn folds_case(&self) -> bool {
72− false
73− }
74−
75− /// True if the working directory's filesystem holds a file named
76− /// `name`, a path component longer than `repo::NAME_MAX` bytes that a
77− /// checkout would create (§7.5 step 1). A filesystem that counts its
78− /// limit in bytes, as ext4 does, holds none.
79− fn name_fits(&self, name: &str) -> bool {
80− name.len() <= crate::repo::NAME_MAX
81− }
82−
83 /// jj tree merge (§7.3): replay the change from->to onto `onto`.
58 /// jj tree merge (§7.3): replay the change from->to onto `onto`.
84 fn replay(
59 fn replay(
85 &self,
60 &self,
@@ -88,12 +63,6 @@ pub trait Backend {
88 to: &[Value],
63 to: &[Value],
89 ) -> Result<Vec<Value>, Crash>;
64 ) -> Result<Vec<Value>, Crash>;
90
65
91− /// Keep snapshot identity available to backends that can reuse stored
92− /// trees. Backends without a tree cache retain their slice-based merge.
93− fn replay_snapshot(&self, onto: &Value, from: &Value, to: &Value) -> Result<Value, Crash> {
94− Ok(Value::list(self.replay(onto.as_list()?, from.as_list()?, to.as_list()?)?))
95− }
96−
97 /// shortest prefix of `id` unique among visible ids, min length 4
66 /// shortest prefix of `id` unique among visible ids, min length 4
98 fn unique_prefix(&self, id: &str) -> String {
67 fn unique_prefix(&self, id: &str) -> String {
99 let others: Vec<String> = self.visible_ids();
68 let others: Vec<String> = self.visible_ids();
@@ -138,56 +107,13 @@ pub fn snapshot_map(entries: &[Value]) -> Result<BTreeMap<Vec<String>, Value>, C
138 .map_err(|_| Crash::new("replay: not a well-formed snapshot (path component not Text)"))
107 .map_err(|_| Crash::new("replay: not a well-formed snapshot (path component not Text)"))
139 })
108 })
140 .collect::<Result<Vec<String>, Crash>>()?;
109 .collect::<Result<Vec<String>, Crash>>()?;
141− // `./` is the root directory, which no entry can be
142− if path.is_empty() {
143− return Err(Crash::new(
144− "replay: not a well-formed snapshot (an entry at the root path ./)",
145− ));
146− }
147 if m.insert(path, content).is_some() {
110 if m.insert(path, content).is_some() {
148 return Err(Crash::new("replay: not a well-formed snapshot (duplicate paths)"));
111 return Err(Crash::new("replay: not a well-formed snapshot (duplicate paths)"));
149 }
112 }
150 }
113 }
151− if let Some(p) = file_and_directory(m.keys()) {
152− return Err(Crash::new(format!(
153− "replay: not a well-formed snapshot (`{}` is both a file and a directory)",
154− p.join("/")
155− )));
156− }
157 Ok(m)
114 Ok(m)
158}
115}
159
116
160−/// The first of `sorted` paths that is also a directory of another: a tree
161−/// holds `a` as a file or as a directory, never both (§7.3). A path's
162−/// descendants sort directly after it (`a` < `a/b` < `a.txt`), so comparing
163−/// each path with the next finds every clash.
164−pub fn file_and_directory<'a>(
165− sorted: impl IntoIterator<Item = &'a Vec<String>>,
166−) -> Option<&'a [String]> {
167− let mut prev: Option<&'a Vec<String>> = None;
168− for p in sorted {
169− if let Some(q) = prev {
170− if p.len() > q.len() && p.starts_with(q) {
171− return Some(q);
172− }
173− }
174− prev = Some(p);
175− }
176− None
177−}
178−
179−/// The crash of a replay whose result would make `path` a file on one side
180−/// and a directory on another, naming the path: one entry cannot list the
181−/// directory's entries (§7.3). The jj backend merges such a path as one
182−/// value and the in-memory one path by path, so they need not refuse the
183−/// same merges (§10).
184−pub fn file_directory_clash(path: &[String]) -> Crash {
185− Crash::new(format!(
186− "replay: `{}` would be a file on one side and a directory on another",
187− path.join("/")
188− ))
189−}
190−
191pub fn map_to_snapshot(m: BTreeMap<Vec<String>, Value>) -> Vec<Value> {
117pub fn map_to_snapshot(m: BTreeMap<Vec<String>, Value>) -> Vec<Value> {
192 m.into_iter()
118 m.into_iter()
193 .map(|(path, content)| {
119 .map(|(path, content)| {
@@ -221,7 +147,7 @@ fn empty_blob() -> Value {
221 }))
147 }))
222}
148}
223
149
224−fn conflict_blob(sides: Vec<Option<ConflictSide>>) -> Value {
150+fn conflict_blob(sides: Vec<Rc<Vec<u8>>>) -> Value {
225 Value::Blob(Rc::new(BlobVal {
151 Value::Blob(Rc::new(BlobVal {
226 kind: BlobKind::Regular,
152 kind: BlobKind::Regular,
227 content: BlobContent::Conflict(sides),
153 content: BlobContent::Conflict(sides),
@@ -276,40 +202,28 @@ pub fn simple_replay(
276 } else if !t_present && !o_present {
202 } else if !t_present && !o_present {
277 // both deleted
203 // both deleted
278 } else {
204 } else {
279− // conflict: sides [to, from, onto] (add, remove, add),
205+ // conflict: sides [to, from, onto] (add, remove, add)
280− // each absent or with its own file type (§7.3)
281− let side = |v: &Value, present: bool| -> Result<Option<ConflictSide>, Crash> {
206+ let get_bytes = |v: &Value, present: bool| -> Result<Rc<Vec<u8>>, Crash> {
282 if !present {
207 if !present {
283− return Ok(None);
208+ return Ok(Rc::new(Vec::new()));
284 }
209 }
285 match v {
210 match v {
286− Value::Blob(b) => Ok(Some(ConflictSide {
287− kind: b.kind.clone(),
288− bytes: Rc::new(b.bytes()?),
211+ Value::Blob(b) => Ok(Rc::new(b.bytes()?)),
289− tree: None,
212+ _ => Ok(Rc::new(Vec::new())),
290− })),
291− _ => Ok(Some(ConflictSide::regular(&[]))),
292 }
213 }
293 };
214 };
294 out.insert(
215 out.insert(
295 p,
216 p,
296 conflict_blob(vec![
217 conflict_blob(vec![
297− side(&t, t_present)?,
218+ get_bytes(&t, t_present)?,
298− side(&f, f_present)?,
219+ get_bytes(&f, f_present)?,
299− side(&o, o_present)?,
220+ get_bytes(&o, o_present)?,
300 ]),
221 ]),
301 );
222 );
302 }
223 }
303 }
224 }
304 }
225 }
305 }
226 }
306− // a file at a path and entries below it cannot be one snapshot. jj
307− // keeps the path as one conflict with the directory as a side, and
308− // refuses that unless one of the three held a conflict there; this
309− // merge keeps no directory as a side, so it always refuses (§7.3, §10)
310− if let Some(p) = file_and_directory(out.keys()) {
311− return Err(file_directory_clash(p));
312− }
313 Ok(map_to_snapshot(out))
227 Ok(map_to_snapshot(out))
314}
228}
315
229
@@ -323,47 +237,17 @@ pub struct MemBackend {
323 pub metas: HashMap<String, MetaInfo>,
237 pub metas: HashMap<String, MetaInfo>,
324 /// id -> parent change ids (first parent first)
238 /// id -> parent change ids (first parent first)
325 pub parents: HashMap<String, Vec<String>>,
239 pub parents: HashMap<String, Vec<String>>,
326− /// Answer `has_conflict` and `is_empty` from the two maps below instead of
327− /// returning None, the way the jj backend answers them from tree ids. A
328− /// backend that answers them takes different paths through rendering and
329− /// never forces a commit's file list, so tests that only ever used the
330− /// None-answering default could not reach those paths at all.
331− pub answers_tree_queries: bool,
332− /// id -> the commit's tree holds a conflict
333− pub conflicts: HashMap<String, bool>,
334− /// id -> the commit's tree equals its first parent's
335− pub empties: HashMap<String, bool>,
336− /// what `folds_case` answers: a working directory on a case-folding
337− /// filesystem, which no test machine need have
338− pub folds_case: bool,
339}
240}
340
241
341impl MemBackend {
242impl MemBackend {
342 pub fn new() -> Self {
243 pub fn new() -> Self {
343− MemBackend::default()
344− }
345−
346− /// A backend that answers the tree queries, like the jj backend does.
347− pub fn answering() -> Self {
348 MemBackend {
244 MemBackend {
349− answers_tree_queries: true,
245+ metas: HashMap::new(),
350− ..MemBackend::default()
246+ parents: HashMap::new(),
351 }
247 }
352 }
248 }
353}
249}
354
250
355−/// A snapshot that is only materialised on first use, the way the jj backend
356−/// builds a commit's `files` (§7.2): tagged as the stored tree of commit `id`,
357−/// so a backend's answers about that commit apply while it holds this list.
358−/// The laziness must stay invisible to the language, which is exactly what is
359−/// easy to get wrong, so tests need to be able to build repos this way. The
360−/// tree has no name (`ThunkVal::tree`): nothing here makes `entries` a tree.
361−pub fn lazy_files(id: &str, entries: Vec<Value>) -> Value {
362− Value::Thunk(Rc::new(crate::value::ThunkVal::stored(id.to_string(), None, move || {
363− Ok(Value::list(entries))
364− })))
365−}
366−
367impl Backend for MemBackend {
251impl Backend for MemBackend {
368 fn visible_ids(&self) -> Vec<String> {
252 fn visible_ids(&self) -> Vec<String> {
369 let mut ids: Vec<String> = self.metas.keys().cloned().collect();
253 let mut ids: Vec<String> = self.metas.keys().cloned().collect();
@@ -374,30 +258,11 @@ impl Backend for MemBackend {
374 self.metas
258 self.metas
375 .get(id)
259 .get(id)
376 .cloned()
260 .cloned()
377− .ok_or_else(|| Crash::new(format!("meta: no stored commit for `{}`", id)))
261+ .ok_or_else(|| Crash::new(format!("meta: no stored commit for id")))
378 }
262 }
379 fn is_merge(&self, id: &str) -> bool {
263 fn is_merge(&self, id: &str) -> bool {
380 self.parents.get(id).map(|p| p.len() >= 2).unwrap_or(false)
264 self.parents.get(id).map(|p| p.len() >= 2).unwrap_or(false)
381 }
265 }
382− fn has_conflict(&self, id: &str) -> Option<bool> {
383− if !self.answers_tree_queries {
384− return None;
385− }
386− Some(self.conflicts.get(id).copied().unwrap_or(false))
387− }
388− fn is_empty(&self, id: &str, parent: &str) -> Option<bool> {
389− if !self.answers_tree_queries {
390− return None;
391− }
392− // `empties` records each commit against its first parent only
393− if self.parents.get(id)?.first()? != parent {
394− return None;
395− }
396− Some(self.empties.get(id).copied().unwrap_or(false))
397− }
398− fn folds_case(&self) -> bool {
399− self.folds_case
400− }
401 fn ancestors_closed(&self, ids: &BTreeSet<String>) -> BTreeSet<String> {
266 fn ancestors_closed(&self, ids: &BTreeSet<String>) -> BTreeSet<String> {
402 let mut seen = BTreeSet::new();
267 let mut seen = BTreeSet::new();
403 let mut stack: Vec<String> = ids.iter().cloned().collect();
268 let mut stack: Vec<String> = ids.iter().cloned().collect();
src/eval.rs+304−810
@@ -4,7 +4,7 @@
4use crate::ast::{Expr, Pattern};
4use crate::ast::{Expr, Pattern};
5use crate::domain::Backend;
5use crate::domain::Backend;
6use crate::shape::{check as contract_check, Contract, Shapes};
6use crate::shape::{check as contract_check, Contract, Shapes};
7−use crate::value::{Crash, Env, FunVal, ListVal, Value};
7+use crate::value::{Crash, Env, FunVal, Value};
8use std::cell::RefCell;
8use std::cell::RefCell;
9use std::collections::HashMap;
9use std::collections::HashMap;
10use std::rc::Rc;
10use std::rc::Rc;
@@ -20,18 +20,8 @@ pub struct Interp {
20 fresh: RefCell<u64>,
20 fresh: RefCell<u64>,
21 /// the currently executing top-level definition (for crash reports)
21 /// the currently executing top-level definition (for crash reports)
22 pub current_def: RefCell<Option<String>>,
22 pub current_def: RefCell<Option<String>>,
23− /// The definitions written as lambdas, by the address of the lambda's
24− /// body, which every closure made from it shares: while that body runs,
25− /// the definition is executing (§1.4), whatever name its closure has
26− /// been bound to since (`prev = goto parents` runs goto's body). The body
27− /// is held so its address cannot be reused.
28− def_bodies: HashMap<*const Expr, (Rc<str>, Rc<Expr>)>,
29 /// the repository as loaded, before snapshotting (for `validate`)
23 /// the repository as loaded, before snapshotting (for `validate`)
30 pub old_repo: RefCell<Option<Value>>,
24 pub old_repo: RefCell<Option<Value>>,
31− /// the repository the expression was given (§1.2 step 4, after the
32− /// snapshot), where displaying a `Commit` reads where it stands (§5.1);
33− /// `old_repo` when unset
34− pub given_repo: RefCell<Option<Value>>,
35 /// Results of applying a config revset the binary itself calls (`trunk`,
25 /// Results of applying a config revset the binary itself calls (`trunk`,
36 /// `immutable`), keyed by the identity of the Repo record they were
26 /// `immutable`), keyed by the identity of the Repo record they were
37 /// applied to. Both are pure and both are asked for more than once per
27 /// applied to. Both are pure and both are asked for more than once per
@@ -40,56 +30,6 @@ pub struct Interp {
40 /// 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
41 /// 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).
42 revsets: RefCell<Vec<(&'static str, Rc<crate::value::RecordMap>, Value)>>,
32 revsets: RefCell<Vec<(&'static str, Rc<crate::value::RecordMap>, Value)>>,
43− /// visible ids in sorted order, built on first use. The backend's
44− /// `visible_ids` allocates every id afresh, and a shortest-unique-prefix
45− /// needs the whole set, so computing one per displayed commit was
46− /// quadratic in both allocations and comparisons.
47− sorted_ids: RefCell<Option<Rc<Vec<String>>>>,
48−}
49−
50−impl Drop for Interp {
51− fn drop(&mut self) {
52− // These are the interpreter's language-value roots. Release them
53− // before collecting, so config definitions and cached closures do
54− // not keep their recursive environments alive between invocations.
55− self.globals = Env::empty();
56− self.old_repo.get_mut().take();
57− self.given_repo.get_mut().take();
58− self.revsets.get_mut().clear();
59− crate::value::collect_cycles();
60− }
61−}
62−
63−/// length of the common leading run of two ids (ids are ASCII, so bytes)
64−fn common_prefix_len(a: &str, b: &str) -> usize {
65− a.bytes()
66− .zip(b.bytes())
67− .take_while(|(x, y)| x == y)
68− .count()
69−}
70−
71−/// The shortest prefix of `id` that no other id in `sorted` shares, at least
72−/// four characters long. Only the neighbours of `id` in sorted order can share
73−/// its longest prefix, so two lookups settle it.
74−pub fn unique_prefix_in(sorted: &[String], id: &str) -> String {
75− let pos = sorted.partition_point(|o| o.as_str() < id);
76− let mut shared = 0usize;
77− if pos > 0 {
78− shared = shared.max(common_prefix_len(&sorted[pos - 1], id));
79− }
80− let mut next = pos;
81− while next < sorted.len() && sorted[next] == id {
82− next += 1;
83− }
84− if next < sorted.len() {
85− shared = shared.max(common_prefix_len(&sorted[next], id));
86− }
87− let n = 4.max(shared + 1);
88− if n >= id.len() {
89− id.to_string()
90− } else {
91− id[..n].to_string()
92− }
93}
33}
94
34
95/// machine state
35/// machine state
@@ -107,28 +47,26 @@ pub(crate) enum Cont {
107 Arg(Rc<Expr>, Env, Rc<Cont>),
47 Arg(Rc<Expr>, Env, Rc<Cont>),
108 Fun(Value, Rc<Cont>),
48 Fun(Value, Rc<Cont>),
109 If(Rc<Expr>, Rc<Expr>, Env, Rc<Cont>),
49 If(Rc<Expr>, Rc<Expr>, Env, Rc<Cont>),
110− /// A block's bindings in evaluation order. Only recursive blocks need a
111− /// cell; acyclic ones extend the environment after each value is ready.
112 LetRest {
50 LetRest {
113− bindings: Rc<[(String, Rc<Expr>)]>,
51+ names: Vec<String>,
114− at: usize,
52+ exprs: Vec<Rc<Expr>>,
53+ acc: Vec<(String, Value)>,
115− cell: Option<Rc<RefCell<Vec<Value>>>>,
54+ cell: Rc<RefCell<Vec<(String, Value)>>>,
116 body: Rc<Expr>,
55 body: Rc<Expr>,
117 env: Env,
56 env: Env,
118 cont: Rc<Cont>,
57 cont: Rc<Cont>,
119 },
58 },
120− /// `&&` or `||` after applying its first operand normally, so the
59+ /// short-circuit ops with the unevaluated rhs
121− /// declared contract has checked it; the rhs is still unevaluated
60+ And(Rc<Expr>, Env, Rc<Cont>),
122− ShortCircuit {
61+ Or2(Rc<Expr>, Env, Rc<Cont>),
62+ /// check the rhs of && / || evaluated to a Bool
123− op: &'static str,
63+ ExpectBool(&'static str, Rc<Cont>),
124− rhs: Rc<Expr>,
125− env: Env,
126− cont: Rc<Cont>,
127− },
128 /// `or` after both evaluated as functions: build the lifted function
64 /// `or` after both evaluated as functions: build the lifted function
129 OrBoth(Value, Rc<Cont>),
65 OrBoth(Value, Rc<Cont>),
130 /// a shared continuation (used when both `or` outcomes continue the same way)
66 /// a shared continuation (used when both `or` outcomes continue the same way)
131 Shared(Rc<Cont>),
67 Shared(Rc<Cont>),
68+ /// unconditionally crash with a fixed message (deferred-body failure)
69+ CrashWith(String),
132 /// run the continuation's computation, catching crashes: on success pass
70 /// run the continuation's computation, catching crashes: on success pass
133 /// the value to `on_ok`, on crash switch to `on_crash` (§4.6)
71 /// the value to `on_ok`, on crash switch to `on_crash` (§4.6)
134 Try {
72 Try {
@@ -136,29 +74,27 @@ pub(crate) enum Cont {
136 snapshot: u64,
74 snapshot: u64,
137 on_crash: Box<State>,
75 on_crash: Box<State>,
138 },
76 },
139− /// apply the returned function to this argument: the `labelled "name"`
77+ /// applying a Labelled: apply inner `labelled` result to the real arg
140− /// of a Labelled to the real arg
141− ApplyTo(Value, Rc<Cont>),
78+ LabelledArg(Value, Rc<Cont>),
142− // Literal/update inputs and accumulators are shared across continuation
143− // clones. The trampoline keeps an active clone for crash unwinding; deep
144− // copying these buffers at every element made flat literals quadratic.
145− UpdateBase(Rc<[(String, Rc<Expr>)]>, Env, Rc<Cont>),
79+ UpdateBase(Vec<(String, Rc<Expr>)>, Env, Rc<Cont>),
146 UpdateFields {
80 UpdateFields {
147 base: Value,
81 base: Value,
148− fields: Rc<[(String, Rc<Expr>)]>,
82+ names: Vec<String>,
83+ exprs: Vec<Rc<Expr>>,
149− acc: Rc<RefCell<Vec<(String, Value)>>>,
84+ acc: Vec<(String, Value)>,
150 env: Env,
85 env: Env,
151 cont: Rc<Cont>,
86 cont: Rc<Cont>,
152 },
87 },
153 RecordFields {
88 RecordFields {
154− fields: Rc<[(String, Rc<Expr>)]>,
89+ names: Vec<String>,
90+ exprs: Vec<Rc<Expr>>,
155− acc: Rc<RefCell<Vec<(String, Value)>>>,
91+ acc: Vec<(String, Value)>,
156 env: Env,
92 env: Env,
157 cont: Rc<Cont>,
93 cont: Rc<Cont>,
158 },
94 },
159 ListElems {
95 ListElems {
160− exprs: Rc<[Rc<Expr>]>,
96+ exprs: Vec<Rc<Expr>>,
161− acc: Rc<RefCell<Vec<Value>>>,
97+ acc: Vec<Value>,
162 env: Env,
98 env: Env,
163 cont: Rc<Cont>,
99 cont: Rc<Cont>,
164 },
100 },
@@ -168,16 +104,6 @@ pub(crate) enum Cont {
168 cexpr: Rc<crate::shape::ContractExpr>,
104 cexpr: Rc<crate::shape::ContractExpr>,
169 cont: Rc<Cont>,
105 cont: Rc<Cont>,
170 },
106 },
171− /// the function a signed definition's value returned when it ran out of
172− /// parameters before its signature (a lambda's body, or a builtin or a
173− /// composition given its last argument): make it the named partial
174− /// application of `args` (§5.2), checked against the rest of `cexpr`
175− NamePartial {
176− name: String,
177− cexpr: Rc<crate::shape::ContractExpr>,
178− args: Vec<Value>,
179− cont: Rc<Cont>,
180− },
181 /// after applying, check the (partial) result against the contract
107 /// after applying, check the (partial) result against the contract
182 #[allow(dead_code)]
108 #[allow(dead_code)]
183 CheckResult {
109 CheckResult {
@@ -186,57 +112,6 @@ pub(crate) enum Cont {
186 supplied: usize,
112 supplied: usize,
187 cont: Rc<Cont>,
113 cont: Rc<Cont>,
188 },
114 },
189− /// the body of a definition's lambda is running (§1.4); passes its value
190− /// on unchanged
191− InDef {
192− name: Rc<str>,
193− cont: Rc<Cont>,
194− },
195− // The builtins that apply functions they are given run as steps of this
196− // machine, not in a nested run, so a recursion through them takes no
197− // native stack (§4.1; Interp::step_applying). Each frame below receives
198− // one value, so `acc` is pushed to once per element and shared, not
199− // copied, when the trampoline clones the continuation.
200− /// `map f xs`, receiving `f` of the element at `idx`
201− MapNext {
202− f: Value,
203− xs: ListVal,
204− idx: usize,
205− acc: Rc<RefCell<Vec<Value>>>,
206− cont: Rc<Cont>,
207− },
208− /// `filter p xs`, receiving `p` of the element at `idx`
209− FilterNext {
210− p: Value,
211− xs: ListVal,
212− idx: usize,
213− acc: Rc<RefCell<Vec<Value>>>,
214− cont: Rc<Cont>,
215− },
216− /// `foldl f z xs`, receiving `f` applied to the accumulator, to be
217− /// applied to the element at `idx`
218− FoldlArg {
219− f: Value,
220− xs: ListVal,
221− idx: usize,
222− cont: Rc<Cont>,
223− },
224− /// `foldl f z xs`, receiving the accumulator after the element at `idx`
225− FoldlAcc {
226− f: Value,
227− xs: ListVal,
228− idx: usize,
229− cont: Rc<Cont>,
230− },
231− /// the result of such a builtin, checked against its contract once the
232− /// functions it applies have run (§4.13); `def` is the signed definition
233− /// the builtin is the value of, which a crash under this frame is in (§1.4)
234− BuiltinResult {
235− cname: String,
236− check: Option<Rc<crate::shape::ContractExpr>>,
237− def: Option<String>,
238− cont: Rc<Cont>,
239− },
240}
115}
241
116
242pub type EResult = Result<Value, Crash>;
117pub type EResult = Result<Value, Crash>;
@@ -251,13 +126,12 @@ pub(crate) enum TryOk {
251 env: Env,
126 env: Env,
252 cont: Rc<Cont>,
127 cont: Rc<Cont>,
253 },
128 },
129+ /// the deferred body of a signed definition: apply it to the pending arg
130+ DeferredBody { arg: Value, cont: Rc<Cont> },
131+ /// the inner leg of a lazy composition: apply the outer function
132+ ComposeInner { f: Value, cont: Rc<Cont> },
254− /// the lhs of a lifted `or` applied to an argument: like `OrLhs`, with
133+ /// the lhs of a lifted `or` applied to an argument
255− /// the rhs function still to be applied to the same argument
256− OrFunLhs {
257− rhs: Value,
258− arg: Value,
259− cont: Rc<Cont>,
134+ OrFunLhs { cont: Rc<Cont> },
260− },
261}
135}
262
136
263pub(crate) enum Run {
137pub(crate) enum Run {
@@ -269,28 +143,23 @@ pub(crate) enum Run {
269/// the parent continuation Rc of `k`, for iterative drop and unwinding
143/// the parent continuation Rc of `k`, for iterative drop and unwinding
270fn parent_rc(k: &Cont) -> Option<&Rc<Cont>> {
144fn parent_rc(k: &Cont) -> Option<&Rc<Cont>> {
271 match k {
145 match k {
272− Cont::Halt | Cont::Try { .. } => None,
146+ Cont::Halt | Cont::CrashWith(_) | Cont::Try { .. } => None,
273 Cont::Arg(_, _, k) => Some(k),
147 Cont::Arg(_, _, k) => Some(k),
274 Cont::Fun(_, k) => Some(k),
148 Cont::Fun(_, k) => Some(k),
275 Cont::If(_, _, _, k) => Some(k),
149 Cont::If(_, _, _, k) => Some(k),
276 Cont::LetRest { cont, .. } => Some(cont),
150 Cont::LetRest { cont, .. } => Some(cont),
277− Cont::ShortCircuit { cont, .. } => Some(cont),
151+ Cont::And(_, _, k) => Some(k),
152+ Cont::Or2(_, _, k) => Some(k),
153+ Cont::ExpectBool(_, k) => Some(k),
278 Cont::OrBoth(_, k) => Some(k),
154 Cont::OrBoth(_, k) => Some(k),
279 Cont::Shared(k) => Some(k),
155 Cont::Shared(k) => Some(k),
280− Cont::ApplyTo(_, k) => Some(k),
156+ Cont::LabelledArg(_, k) => Some(k),
281 Cont::UpdateBase(_, _, k) => Some(k),
157 Cont::UpdateBase(_, _, k) => Some(k),
282 Cont::UpdateFields { cont, .. } => Some(cont),
158 Cont::UpdateFields { cont, .. } => Some(cont),
283 Cont::RecordFields { cont, .. } => Some(cont),
159 Cont::RecordFields { cont, .. } => Some(cont),
284 Cont::ListElems { cont, .. } => Some(cont),
160 Cont::ListElems { cont, .. } => Some(cont),
285 Cont::CheckExpr { cont, .. } => Some(cont),
161 Cont::CheckExpr { cont, .. } => Some(cont),
286− Cont::NamePartial { cont, .. } => Some(cont),
287 Cont::CheckResult { cont, .. } => Some(cont),
162 Cont::CheckResult { cont, .. } => Some(cont),
288− Cont::InDef { cont, .. } => Some(cont),
289− Cont::MapNext { cont, .. } => Some(cont),
290− Cont::FilterNext { cont, .. } => Some(cont),
291− Cont::FoldlArg { cont, .. } => Some(cont),
292− Cont::FoldlAcc { cont, .. } => Some(cont),
293− Cont::BuiltinResult { cont, .. } => Some(cont),
294 }
163 }
295}
164}
296
165
@@ -299,99 +168,6 @@ pub(crate) fn pop_cont(k: &Cont) -> Option<Cont> {
299 parent_rc(k).map(|r| (**r).clone())
168 parent_rc(k).map(|r| (**r).clone())
300}
169}
301
170
302−/// Drop `k`, a continuation a crash abandons, one frame at a time. A frame
303−/// drops its parent with it, so dropping a chain as long as the recursion a
304−/// crash ended took native stack per frame, and a crash at the bottom of a
305−/// deep recursion aborted the process instead of being reported or caught
306−/// (§4.1, §4.7). The walk stops at a frame another chain still holds.
307−fn drop_chain(k: Cont) {
308− let mut parent = parent_rc(&k).cloned();
309− drop(k);
310− while let Some(rc) = parent {
311− parent = match Rc::try_unwrap(rc) {
312− Ok(frame) => parent_rc(&frame).cloned(),
313− Err(_) => None,
314− };
315− }
316−}
317−
318−/// The innermost definition whose application `k` continues (§1.4): a
319−/// signed definition's application runs under the `CheckExpr` of its result,
320−/// the body of a definition's lambda under an `InDef` naming it unless that
321−/// `CheckExpr` names it already, and the functions a builtin applies as a
322−/// signed definition's value under a `BuiltinResult` naming it; each stays on
323−/// the chain until the body has returned. Only used on a crash no `Try`
324−/// caught, so there is no `Try` on the chain to stop the walk early.
325−fn innermost_def(k: &Cont) -> Option<String> {
326− let mut k = k;
327− loop {
328− match k {
329− Cont::CheckExpr { name, .. }
330− | Cont::BuiltinResult {
331− def: Some(name), ..
332− } => return Some(name.clone()),
333− Cont::InDef { name, .. } => return Some(name.to_string()),
334− _ => {}
335− }
336− k = parent_rc(k)?;
337− }
338−}
339−
340−/// True when `k` starts with the frames a body of the signed definition
341−/// `name` pushes when `cexpr` is what remains of its signature: the check of
342−/// its value and, while `cexpr` lists further arguments, the naming of that
343−/// value as the definition's partial application. A body applied under them
344−/// is a call in tail position of the same definition (`shortS (n - 1)` in
345−/// `shortS`'s body), and its own frames would check the same value against
346−/// the same contract under the same name once more and name it afresh, only
347−/// for the outer naming to replace that name (NamePartial). The outer frames
348−/// serve for both, so a signed tail recursion keeps its continuation
349−/// constant in size, as an unsigned one does (§4.1, §4.13).
350−fn checks_already(k: &Cont, name: &str, cexpr: &crate::shape::ContractExpr) -> bool {
351− match k {
352− Cont::CheckExpr { name: n, cexpr: c, cont } if n == name && c.same_as(cexpr) => {
353− cexpr.is_exhausted()
354− || matches!(&**cont, Cont::NamePartial { name: m, cexpr: d, .. }
355− if m == name && d.same_as(cexpr))
356− }
357− _ => false,
358− }
359−}
360−
361−/// the builtins that apply the functions they are given; they run as steps
362−/// of the machine (Interp::step_applying)
363−fn applies_functions(builtin: &str) -> bool {
364− matches!(builtin, "map" | "filter" | "foldl" | "(.)")
365−}
366−
367−/// a builtin's function argument
368−fn function_arg(v: &Value) -> Result<Value, Crash> {
369− match v {
370− Value::Fun(_) => Ok(v.clone()),
371− v => Err(Crash::new(format!(
372− "expected a function, got a {}",
373− v.kind_name()
374− ))),
375− }
376−}
377−
378−/// a builtin's list argument, sharing its elements
379−fn list_arg(v: &Value) -> Result<ListVal, Crash> {
380− match v {
381− Value::List(xs) => Ok(xs.clone()),
382− v => Err(Crash::new(format!("expected a list, got a {}", v.kind_name()))),
383− }
384−}
385−
386−/// `c`, raised while applying the definition `def`, is in it unless
387−/// something nearer, a nested run, already named one (§1.4)
388−fn in_def(mut c: Crash, def: &str) -> Crash {
389− if c.def.is_none() {
390− c.def = Some(def.to_string());
391− }
392− c
393−}
394−
395impl Interp {
171impl Interp {
396 pub fn new(backend: Rc<dyn Backend>, shapes: Shapes, globals: Env) -> Interp {
172 pub fn new(backend: Rc<dyn Backend>, shapes: Shapes, globals: Env) -> Interp {
397 Interp {
173 Interp {
@@ -401,31 +177,11 @@ impl Interp {
401 contracts: HashMap::new(),
177 contracts: HashMap::new(),
402 fresh: RefCell::new(0),
178 fresh: RefCell::new(0),
403 current_def: RefCell::new(None),
179 current_def: RefCell::new(None),
404− def_bodies: HashMap::new(),
405 old_repo: RefCell::new(None),
180 old_repo: RefCell::new(None),
406− given_repo: RefCell::new(None),
407 revsets: RefCell::new(Vec::new()),
181 revsets: RefCell::new(Vec::new()),
408− sorted_ids: RefCell::new(None),
409 }
182 }
410 }
183 }
411
184
412− /// Shortest prefix of `id` unique among the visible ids, minimum four
413− /// (§5.1). The sorted id list is built once per run.
414− pub fn unique_prefix(&self, id: &str) -> String {
415− let cached = self.sorted_ids.borrow().clone();
416− let sorted = match cached {
417− Some(s) => s,
418− None => {
419− let mut ids = self.backend.visible_ids();
420− ids.sort();
421− let rc = Rc::new(ids);
422− *self.sorted_ids.borrow_mut() = Some(rc.clone());
423− rc
424− }
425− };
426− unique_prefix_in(&sorted, id)
427− }
428−
429 /// Apply one of the config revsets the binary calls itself, reusing the
185 /// Apply one of the config revsets the binary calls itself, reusing the
430 /// result when the same Repo value has already been asked (§7.5).
186 /// result when the same Repo value has already been asked (§7.5).
431 pub fn apply_cached_revset(&mut self, name: &'static str, repo: &Value) -> EResult {
187 pub fn apply_cached_revset(&mut self, name: &'static str, repo: &Value) -> EResult {
@@ -461,11 +217,8 @@ impl Interp {
461 contracts: HashMap::new(),
217 contracts: HashMap::new(),
462 fresh: RefCell::new(0),
218 fresh: RefCell::new(0),
463 current_def: RefCell::new(None),
219 current_def: RefCell::new(None),
464− def_bodies: HashMap::new(),
465 old_repo: RefCell::new(None),
220 old_repo: RefCell::new(None),
466− given_repo: RefCell::new(None),
467 revsets: RefCell::new(Vec::new()),
221 revsets: RefCell::new(Vec::new()),
468− sorted_ids: RefCell::new(None),
469 }
222 }
470 }
223 }
471
224
@@ -496,26 +249,6 @@ impl Interp {
496 self.globals.clone()
249 self.globals.clone()
497 }
250 }
498
251
499− /// Record `expr`, the expression of the definition `name`: the body of
500− /// the lambda it is written as runs as `name`'s (§1.4), and so does the
501− /// body of each lambda that body is in turn (`\a -> \b -> e`). A lambda
502− /// in parentheses or ending a `let` is the definition's lambda too.
503− pub fn name_bodies(&mut self, name: &str, expr: &Rc<Expr>) {
504− let name: Rc<str> = Rc::from(name);
505− let mut e = expr;
506− loop {
507− match &**e {
508− Expr::Paren(inner) | Expr::Let(_, inner) => e = inner,
509− Expr::Lambda(_, body, _) => {
510− self.def_bodies
511− .insert(Rc::as_ptr(body), (name.clone(), body.clone()));
512− e = body;
513− }
514− _ => break,
515− }
516− }
517− }
518−
519 /// Main entry: evaluate an expression in an environment.
252 /// Main entry: evaluate an expression in an environment.
520 pub fn eval(&mut self, e: &Rc<Expr>, env: &Env) -> EResult {
253 pub fn eval(&mut self, e: &Rc<Expr>, env: &Env) -> EResult {
521 self.run(State::Eval(e.clone(), env.clone(), Cont::Halt))
254 self.run(State::Eval(e.clone(), env.clone(), Cont::Halt))
@@ -532,105 +265,11 @@ impl Interp {
532 c
265 c
533 }
266 }
534
267
535− /// `c`, raised while applying the builtin `builtin` under the contract
536− /// named `cname`, is in that definition when the builtin is a signed
537− /// definition's value (`squash = abandon . …`). A builtin's own name, or
538− /// a composition's made from its operands', names no definition, and the
539− /// crash is in whichever definition applied it (§1.4).
540− fn blame_builtin(&self, c: Crash, builtin: &str, cname: &str) -> Crash {
541− match self.builtin_def(builtin, cname) {
542− Some(def) => in_def(c, &def),
543− None => c,
544− }
545− }
546−
547− /// the definition a crash while applying `builtin` under the contract
548− /// named `cname` is in, as blame_builtin decides, and which the builtin
549− /// renders as (§5.2)
550− pub(crate) fn builtin_def(&self, builtin: &str, cname: &str) -> Option<String> {
551− if cname != builtin && self.contracts.contains_key(cname) {
552− Some(cname.to_string())
553− } else {
554− None
555− }
556− }
557−
558− /// `map`, `filter`, `foldl` or a composition node given all its
559− /// arguments, continuing in `k`. They apply the functions they are given,
560− /// so they run as steps of this machine: applying them in a nested run
561− /// put every level of a recursion through one of them on the native
562− /// stack, which overflowed and aborted where plain recursion runs (§4.1).
563− /// Evaluation stays left to right, element by element (§4).
564− fn step_applying(&mut self, builtin: &str, args: &[Value], k: Cont) -> Result<Run, Crash> {
565− match builtin {
566− // (f . g) x = f (g x) (§4.9), whatever x is: a function argument
567− // is applied like any other, and g's contract checks it (§4.13).
568− // `squash = abandon . contract everything` waits for the
569− // repository because it is this node, not because anything
570− // composes on a function argument.
571− "(.)" => Ok(Run::Step(State::Apply(
572− args[1].clone(),
573− args[2].clone(),
574− Cont::Fun(args[0].clone(), Rc::new(k)),
575− ))),
576− "map" | "filter" => {
577− let f = function_arg(&args[0])?;
578− let xs = list_arg(&args[1])?;
579− if xs.is_empty() {
580− return Ok(Run::Step(State::Ret(Value::list(vec![]), k)));
581− }
582− let x = xs[0].clone();
583− let acc = Rc::new(RefCell::new(Vec::new()));
584− let cont = Rc::new(k);
585− let next = if builtin == "map" {
586− Cont::MapNext {
587− f: f.clone(),
588− xs,
589− idx: 0,
590− acc,
591− cont,
592− }
593− } else {
594− Cont::FilterNext {
595− p: f.clone(),
596− xs,
597− idx: 0,
598− acc,
599− cont,
600− }
601− };
602− Ok(Run::Step(State::Apply(f, x, next)))
603− }
604− "foldl" => {
605− let f = function_arg(&args[0])?;
606− let xs = list_arg(&args[2])?;
607− if xs.is_empty() {
608− return Ok(Run::Step(State::Ret(args[1].clone(), k)));
609− }
610− let next = Cont::FoldlArg {
611− f: f.clone(),
612− xs,
613− idx: 0,
614− cont: Rc::new(k),
615− };
616− Ok(Run::Step(State::Apply(f, args[1].clone(), next)))
617− }
618− _ => Err(Crash::new(format!(
619− "internal: `{}` does not apply functions",
620− builtin
621− ))),
622− }
623− }
624−
625 fn run(&mut self, st: State) -> EResult {
268 fn run(&mut self, st: State) -> EResult {
626− let result = match self.trampoline(st) {
269+ match self.trampoline(st) {
627 Ok(v) => Ok(v),
270 Ok(v) => Ok(v),
628 Err(c) => Err(self.decorate(c)),
271 Err(c) => Err(self.decorate(c)),
629− };
272+ }
630− // Keep a returned closure alive as a root while reclaiming local
631− // recursive environments abandoned by the completed evaluation.
632− crate::value::collect_cycles();
633− result
634 }
273 }
635
274
636 fn trampoline(&mut self, st: State) -> EResult {
275 fn trampoline(&mut self, st: State) -> EResult {
@@ -657,18 +296,26 @@ impl Interp {
657 Run::Step(next) => st = next,
296 Run::Step(next) => st = next,
658 Run::Done(v) => return Ok(v),
297 Run::Done(v) => return Ok(v),
659 Run::Crash(c) => {
298 Run::Crash(c) => {
660− // Unwind to the nearest Try continuation, restoring the
299+ // unwind to the nearest Try continuation, restoring the
661 // fresh-id snapshot so a caught crash leaves no trace.
300 // fresh-id snapshot so a caught crash leaves no trace.
662− // A Try cannot catch a crash raised by its own handler:
301+ // A `Shared` continuation links back to a context already
663− // every `on_crash` state continues through `Cont::Shared`
302+ // containing a Try, so track visited Try nodes and skip
664− // holding the continuation *outside* the Try, so the Try
303+ // ones already unwound past (a caught crash must not be
665− // is no longer on the chain once the handler runs.
304+ // re-caught by the same Try, which would loop forever).
666 let mut k = Some(active.clone());
305 let mut k = Some(active.clone());
306+ let mut visited: Vec<*const Cont> = Vec::new();
667 let caught = loop {
307 let caught = loop {
668 match k {
308 match k {
669 Some(Cont::Try {
309 Some(Cont::Try {
670 snapshot, on_crash, ..
310 snapshot, on_crash, ..
671 }) => {
311 }) => {
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+ }
672 *self.fresh.borrow_mut() = snapshot;
319 *self.fresh.borrow_mut() = snapshot;
673 break Some(*on_crash);
320 break Some(*on_crash);
674 }
321 }
@@ -676,20 +323,9 @@ impl Interp {
676 None => break None,
323 None => break None,
677 }
324 }
678 };
325 };
679− // the frames up to the Try, or all of them, are abandoned
680 match caught {
326 match caught {
681− Some(next) => {
327+ Some(next) => st = next,
682− drop_chain(active);
683− st = next
684− }
685− None => {
686− let mut c = c;
687− if c.def.is_none() {
688− c.def = innermost_def(&active);
689− }
690− drop_chain(active);
691− return Err(c);
328+ None => return Err(c),
692− }
693 }
329 }
694 }
330 }
695 }
331 }
@@ -740,6 +376,7 @@ impl Interp {
740 params: params.clone(),
376 params: params.clone(),
741 applied: 0,
377 applied: 0,
742 applied_args: Vec::new(),
378 applied_args: Vec::new(),
379+ deferred: false,
743 body: body.clone(),
380 body: body.clone(),
744 env,
381 env,
745 src: src.clone(),
382 src: src.clone(),
@@ -756,31 +393,18 @@ impl Interp {
756 if bs.is_empty() {
393 if bs.is_empty() {
757 return Run::Step(State::Eval(body.clone(), env, k));
394 return Run::Step(State::Eval(body.clone(), env, k));
758 }
395 }
759− // the bindings are evaluated in dependency order, not source
396+ let names: Vec<String> = bs.iter().map(|(n, _)| n.clone()).collect();
760− // order, and a cycle among them is a crash (§4.1)
761− let plan = match crate::config::let_plan(bs) {
762− Some(plan) => plan,
763− None => return Run::Crash(Crash::new("let: a cycle among the bindings")),
764− };
765− let bindings: Rc<[(String, Rc<Expr>)]> =
766− plan.order.iter().map(|&i| bs[i].clone()).collect();
397+ let exprs: Vec<Rc<Expr>> = bs.iter().map(|(_, e)| e.clone()).collect();
767− // A closure in a recursive block sees later bindings through
398+ // one recursive frame shared by every binding and the body, so
768− // the shared cell (§4.1). Acyclic bindings need only earlier
399+ // the block is mutually recursive (§4.1)
769− // values: immutable frames avoid a cell -> closure -> frame
770− // cycle for ordinary local helper functions.
771− let (env2, cell) = if plan.recursive {
772− let names = bindings.iter().map(|(n, _)| n.clone()).collect();
773− let (env, cell) = env.extend_rec(names);
400+ let (env2, cell) = env.extend_rec();
774− (env, Some(cell))
775− } else {
776− (env, None)
777− };
778 Run::Step(State::Eval(
401 Run::Step(State::Eval(
779− bindings[0].1.clone(),
402+ exprs[0].clone(),
780 env2.clone(),
403 env2.clone(),
781 Cont::LetRest {
404 Cont::LetRest {
782− bindings,
405+ names,
783− at: 0,
406+ exprs,
407+ acc: Vec::new(),
784 cell,
408 cell,
785 body: body.clone(),
409 body: body.clone(),
786 env: env2,
410 env: env2,
@@ -794,26 +418,29 @@ impl Interp {
794 Cont::Arg(x.clone(), env, Rc::new(k)),
418 Cont::Arg(x.clone(), env, Rc::new(k)),
795 )),
419 )),
796 Expr::BinOp(op, a, b) => {
420 Expr::BinOp(op, a, b) => {
421+ if *op == "&&" {
422+ return Run::Step(State::Eval(
423+ a.clone(),
424+ env.clone(),
425+ Cont::And(b.clone(), env, Rc::new(k)),
426+ ));
427+ }
428+ if *op == "||" {
429+ return Run::Step(State::Eval(
430+ a.clone(),
431+ env.clone(),
432+ Cont::Or2(b.clone(), env, Rc::new(k)),
433+ ));
434+ }
797 let fv = match env.lookup(op) {
435 let fv = match env.lookup(op) {
798 Some(v) => v,
436 Some(v) => v,
799 None => return Run::Crash(Crash::new(format!("unbound name `{}`", op))),
437 None => return Run::Crash(Crash::new(format!("unbound name `{}`", op))),
800 };
438 };
801− let next = if *op == "&&" || *op == "||" {
439+ // eval a, apply the operator to it, then apply the result to b
802− Cont::ShortCircuit {
803− op,
804− rhs: b.clone(),
805− env: env.clone(),
806− cont: Rc::new(k),
807− }
808− } else {
809− Cont::Arg(b.clone(), env.clone(), Rc::new(k))
810− };
811− // Every operator must be declared, and its first operand
812− // is checked by ordinary application, including && and ||.
813 Run::Step(State::Eval(
440 Run::Step(State::Eval(
814 a.clone(),
441 a.clone(),
815− env,
442+ env.clone(),
816− Cont::Fun(fv, Rc::new(next)),
443+ Cont::Fun(fv, Rc::new(Cont::Arg(b.clone(), env, Rc::new(k)))),
817 ))
444 ))
818 }
445 }
819 Expr::Or(a, b) => {
446 Expr::Or(a, b) => {
@@ -842,19 +469,22 @@ impl Interp {
842 Expr::Update(base, fields) => Run::Step(State::Eval(
469 Expr::Update(base, fields) => Run::Step(State::Eval(
843 base.clone(),
470 base.clone(),
844 env.clone(),
471 env.clone(),
845− Cont::UpdateBase(fields.clone().into(), env, Rc::new(k)),
472+ Cont::UpdateBase(fields.clone(), env, Rc::new(k)),
846 )),
473 )),
847 Expr::Record(fields) => {
474 Expr::Record(fields) => {
848 if fields.is_empty() {
475 if fields.is_empty() {
849 let m = crate::value::RecordMap::new();
476 let m = crate::value::RecordMap::new();
850 return Run::Step(State::Ret(Value::Record(Rc::new(m)), k));
477 return Run::Step(State::Ret(Value::Record(Rc::new(m)), k));
851 }
478 }
479+ let names: Vec<String> = fields.iter().map(|(n, _)| n.clone()).collect();
480+ let exprs: Vec<Rc<Expr>> = fields.iter().map(|(_, v)| v.clone()).collect();
852 Run::Step(State::Eval(
481 Run::Step(State::Eval(
853− fields[0].1.clone(),
482+ exprs[0].clone(),
854 env.clone(),
483 env.clone(),
855 Cont::RecordFields {
484 Cont::RecordFields {
856− fields: fields.clone().into(),
485+ names,
857− acc: Rc::new(RefCell::new(Vec::with_capacity(fields.len()))),
486+ exprs,
487+ acc: Vec::new(),
858 env,
488 env,
859 cont: Rc::new(k),
489 cont: Rc::new(k),
860 },
490 },
@@ -868,8 +498,8 @@ impl Interp {
868 elems[0].clone(),
498 elems[0].clone(),
869 env.clone(),
499 env.clone(),
870 Cont::ListElems {
500 Cont::ListElems {
871− exprs: elems.clone().into(),
501+ exprs: elems.clone(),
872− acc: Rc::new(RefCell::new(Vec::with_capacity(elems.len()))),
502+ acc: Vec::new(),
873 env,
503 env,
874 cont: Rc::new(k),
504 cont: Rc::new(k),
875 },
505 },
@@ -895,31 +525,29 @@ impl Interp {
895 ))),
525 ))),
896 },
526 },
897 Cont::LetRest {
527 Cont::LetRest {
898− bindings,
528+ names,
899− at,
529+ exprs,
530+ mut acc,
900 cell,
531 cell,
901 body,
532 body,
902 env,
533 env,
903 cont,
534 cont,
904 } => {
535 } => {
905− let env = match &cell {
536+ let idx = acc.len();
906− Some(cell) => {
537+ let pair = (names[idx].clone(), v);
907− cell.borrow_mut().push(v);
538+ cell.borrow_mut().push(pair.clone());
908− env
539+ acc.push(pair);
909− }
540+ if acc.len() == names.len() {
910− None => env.extend(vec![(bindings[at].0.clone(), v)]),
911− };
912− let at = at + 1;
913− if at == bindings.len() {
914 Run::Step(State::Eval(body, env, (*cont).clone()))
541 Run::Step(State::Eval(body, env, (*cont).clone()))
915 } else {
542 } else {
916− let next = bindings[at].1.clone();
543+ let next = exprs[acc.len()].clone();
917 Run::Step(State::Eval(
544 Run::Step(State::Eval(
918 next,
545 next,
919 env.clone(),
546 env.clone(),
920 Cont::LetRest {
547 Cont::LetRest {
921− bindings,
548+ names,
922− at,
549+ exprs,
550+ acc,
923 cell,
551 cell,
924 body,
552 body,
925 env,
553 env,
@@ -928,42 +556,24 @@ impl Interp {
928 ))
556 ))
929 }
557 }
930 }
558 }
931− Cont::ShortCircuit { op, rhs, env, cont } => {
559+ Cont::And(b, env, k) => match v {
932− // Operator names are reserved: resolving && or || and
560+ Value::Bool(false) => Run::Step(State::Ret(Value::Bool(false), (*k).clone())),
933− // applying one operand always gives its builtin partial.
561+ Value::Bool(true) => Run::Step(State::Eval(b, env, Cont::ExpectBool("&&", k))),
934− let (lhs, pending) = match &v {
562+ _ => Run::Crash(Crash::new(format!("&&: expected Bool, got a {}", v.kind_name()))),
935− Value::Fun(f) => match &**f {
563+ },
936− FunVal::Builtin { args, pending, .. } if args.len() == 1 => {
564+ Cont::Or2(b, env, k) => match v {
937− (&args[0], pending)
565+ Value::Bool(true) => Run::Step(State::Ret(Value::Bool(true), (*k).clone())),
938− }
566+ Value::Bool(false) => Run::Step(State::Eval(b, env, Cont::ExpectBool("||", k))),
939− _ => return Run::Crash(Crash::new("internal: invalid boolean operator")),
567+ _ => Run::Crash(Crash::new(format!("||: expected Bool, got a {}", v.kind_name()))),
940− },
941− _ => return Run::Crash(Crash::new("internal: invalid boolean operator")),
942− };
568+ },
943− let lhs = match lhs {
569+ Cont::ExpectBool(op, k) => match v {
944− Value::Bool(b) => *b,
570+ Value::Bool(_) => Run::Step(State::Ret(v, (*k).clone())),
945− other => return Run::Crash(Crash::new(format!(
571+ _ => Run::Crash(Crash::new(format!(
946− "{}: expected Bool, got a {}", op, other.kind_name()
572+ "{}: expected Bool, got a {}",
947− ))),
573+ op,
948− };
574+ v.kind_name()
949− if lhs == (op == "||") {
575+ ))),
950− let result = Value::Bool(lhs);
951− if let Some((name, c, _)) = pending {
952− // Skipping an operand must not evaluate or check
953− // it, but the operator still returns a checked
954− // result after its two parameter positions (§4.13).
955− let c = crate::shape::ContractExpr::apply_first_rc(c.clone());
956− if let Err(cr) = c.check_result(&self.shapes, name, &result) {
957− return Run::Crash(self.blame_builtin(cr, op, name));
958− }
959− }
960− Run::Step(State::Ret(result, (*cont).clone()))
961− } else {
962− // The normal application path checks the rhs and the
963− // builtin's result, with the same argument numbering.
964− Run::Step(State::Eval(rhs, env, Cont::Fun(v, cont)))
965− }
966− }
576+ },
967 Cont::OrBoth(a, k) => {
577 Cont::OrBoth(a, k) => {
968 if matches!(v, Value::Fun(_)) {
578 if matches!(v, Value::Fun(_)) {
969 Run::Step(State::Ret(Value::Fun(Rc::new(FunVal::OrFun(a, v))), (*k).clone()))
579 Run::Step(State::Ret(Value::Fun(Rc::new(FunVal::OrFun(a, v))), (*k).clone()))
@@ -994,24 +604,16 @@ impl Interp {
994 Run::Step(State::Ret(v, Cont::Shared(cont)))
604 Run::Step(State::Ret(v, Cont::Shared(cont)))
995 }
605 }
996 }
606 }
997− Some(TryOk::OrFunLhs { rhs, arg, cont }) => {
607+ Some(TryOk::DeferredBody { arg, cont, .. }) => {
998− // `f x or g x` (§4.6): when `f x` is itself a
999− // function, apply `g` too and lift again, so the
1000− // fallback keeps guarding the later arguments
1001− if matches!(v, Value::Fun(_)) {
1002− Run::Step(State::Apply(
608+ Run::Step(State::Apply(v, arg, (*cont).clone()))
1003− rhs,
1004− arg,
1005− Cont::OrBoth(v, Rc::new(Cont::Shared(cont))),
1006− ))
1007− } else {
1008− Run::Step(State::Ret(v, (*cont).clone()))
1009− }
1010 }
609 }
610+ Some(TryOk::ComposeInner { f, cont }) => Run::Step(State::Apply(f, v, (*cont).clone())),
611+ Some(TryOk::OrFunLhs { cont, .. }) => Run::Step(State::Ret(v, (*cont).clone())),
1011 None => Run::Crash(Crash::new("internal: Try with no success handler")),
612 None => Run::Crash(Crash::new("internal: Try with no success handler")),
1012 }
613 }
1013 }
614 }
1014− Cont::ApplyTo(real, k) => Run::Step(State::Apply(v, real, (*k).clone())),
615+ Cont::LabelledArg(real, k) => Run::Step(State::Apply(v, real, (*k).clone())),
616+ Cont::CrashWith(msg) => Run::Crash(Crash::new(msg)),
1015 Cont::UpdateBase(fields, env, k) => {
617 Cont::UpdateBase(fields, env, k) => {
1016 if !matches!(v, Value::Record(_)) {
618 if !matches!(v, Value::Record(_)) {
1017 return Run::Crash(Crash::new(format!(
619 return Run::Crash(Crash::new(format!(
@@ -1019,16 +621,19 @@ impl Interp {
1019 v.kind_name()
621 v.kind_name()
1020 )));
622 )));
1021 }
623 }
624+ let names: Vec<String> = fields.iter().map(|(n, _)| n.clone()).collect();
625+ let exprs: Vec<Rc<Expr>> = fields.iter().map(|(_, e)| e.clone()).collect();
1022− if fields.is_empty() {
626+ if exprs.is_empty() {
1023 return Run::Step(State::Ret(v, (*k).clone()));
627 return Run::Step(State::Ret(v, (*k).clone()));
1024 }
628 }
1025 Run::Step(State::Eval(
629 Run::Step(State::Eval(
1026− fields[0].1.clone(),
630+ exprs[0].clone(),
1027 env.clone(),
631 env.clone(),
1028 Cont::UpdateFields {
632 Cont::UpdateFields {
1029 base: v,
633 base: v,
1030− acc: Rc::new(RefCell::new(Vec::with_capacity(fields.len()))),
634+ names,
1031− fields,
635+ exprs,
636+ acc: Vec::new(),
1032 env,
637 env,
1033 cont: k,
638 cont: k,
1034 },
639 },
@@ -1036,23 +641,20 @@ impl Interp {
1036 }
641 }
1037 Cont::UpdateFields {
642 Cont::UpdateFields {
1038 base,
643 base,
1039− fields,
644+ names,
645+ exprs,
1040− acc,
646+ mut acc,
1041 env,
647 env,
1042 cont,
648 cont,
1043 } => {
649 } => {
1044− let filled = {
1045− let mut values = acc.borrow_mut();
1046− let idx = values.len();
650+ let idx = acc.len();
1047− values.push((fields[idx].0.clone(), v));
651+ acc.push((names[idx].clone(), v));
1048− values.len()
652+ if acc.len() == names.len() {
1049− };
1050− if filled == fields.len() {
1051 let mut m = match &base {
653 let mut m = match &base {
1052 Value::Record(m) => m.as_ref().clone(),
654 Value::Record(m) => m.as_ref().clone(),
1053 _ => unreachable!(),
655 _ => unreachable!(),
1054 };
656 };
1055− for (n, val) in acc.take() {
657+ for (n, val) in acc {
1056 if !m.contains_key(&n) {
658 if !m.contains_key(&n) {
1057 return Run::Crash(Crash::new(format!(
659 return Run::Crash(Crash::new(format!(
1058 "cannot update: record has no field `{}`",
660 "cannot update: record has no field `{}`",
@@ -1063,13 +665,14 @@ impl Interp {
1063 }
665 }
1064 Run::Step(State::Ret(Value::Record(Rc::new(m)), (*cont).clone()))
666 Run::Step(State::Ret(Value::Record(Rc::new(m)), (*cont).clone()))
1065 } else {
667 } else {
1066− let next = fields[filled].1.clone();
668+ let next = exprs[acc.len()].clone();
1067 Run::Step(State::Eval(
669 Run::Step(State::Eval(
1068 next,
670 next,
1069 env.clone(),
671 env.clone(),
1070 Cont::UpdateFields {
672 Cont::UpdateFields {
1071 base,
673 base,
1072− fields,
674+ names,
675+ exprs,
1073 acc,
676 acc,
1074 env,
677 env,
1075 cont,
678 cont,
@@ -1078,27 +681,25 @@ impl Interp {
1078 }
681 }
1079 }
682 }
1080 Cont::RecordFields {
683 Cont::RecordFields {
1081− fields,
684+ names,
685+ exprs,
1082− acc,
686+ mut acc,
1083 env,
687 env,
1084 cont,
688 cont,
1085 } => {
689 } => {
1086− let filled = {
1087− let mut values = acc.borrow_mut();
1088− let idx = values.len();
690+ let idx = acc.len();
1089− values.push((fields[idx].0.clone(), v));
691+ acc.push((names[idx].clone(), v));
1090− values.len()
692+ if acc.len() == names.len() {
1091− };
1092− if filled == fields.len() {
1093− let m: crate::value::RecordMap = acc.take().into_iter().collect();
693+ let m: crate::value::RecordMap = acc.into_iter().collect();
1094 Run::Step(State::Ret(Value::Record(Rc::new(m)), (*cont).clone()))
694 Run::Step(State::Ret(Value::Record(Rc::new(m)), (*cont).clone()))
1095 } else {
695 } else {
1096− let next = fields[filled].1.clone();
696+ let next = exprs[acc.len()].clone();
1097 Run::Step(State::Eval(
697 Run::Step(State::Eval(
1098 next,
698 next,
1099 env.clone(),
699 env.clone(),
1100 Cont::RecordFields {
700 Cont::RecordFields {
1101− fields,
701+ names,
702+ exprs,
1102 acc,
703 acc,
1103 env,
704 env,
1104 cont,
705 cont,
@@ -1108,19 +709,15 @@ impl Interp {
1108 }
709 }
1109 Cont::ListElems {
710 Cont::ListElems {
1110 exprs,
711 exprs,
1111− acc,
712+ mut acc,
1112 env,
713 env,
1113 cont,
714 cont,
1114 } => {
715 } => {
1115− let filled = {
1116− let mut values = acc.borrow_mut();
1117− values.push(v);
716+ acc.push(v);
1118− values.len()
1119− };
1120− if filled == exprs.len() {
717+ if acc.len() == exprs.len() {
1121− Run::Step(State::Ret(Value::list(acc.take()), (*cont).clone()))
718+ Run::Step(State::Ret(Value::list(acc), (*cont).clone()))
1122 } else {
719 } else {
1123− let next = exprs[filled].clone();
720+ let next = exprs[acc.len()].clone();
1124 Run::Step(State::Eval(
721 Run::Step(State::Eval(
1125 next,
722 next,
1126 env.clone(),
723 env.clone(),
@@ -1139,19 +736,6 @@ impl Interp {
1139 }
736 }
1140 Run::Step(State::Ret(v, (*cont).clone()))
737 Run::Step(State::Ret(v, (*cont).clone()))
1141 }
738 }
1142− Cont::NamePartial {
1143− name,
1144− cexpr,
1145− args,
1146− cont,
1147− } => {
1148− // a value the same definition named already, by a recursive
1149− // call that was no tail call (`let r = f (n - 1) in r`), is
1150− // named afresh rather than wrapped once more per call
1151− let v = crate::value::named_inner(&v, &name, &cexpr).unwrap_or(v);
1152− let named = crate::value::named_apply(v, args, &name, cexpr);
1153− Run::Step(State::Ret(named, (*cont).clone()))
1154− }
1155 Cont::CheckResult {
739 Cont::CheckResult {
1156 name,
740 name,
1157 contract,
741 contract,
@@ -1176,96 +760,6 @@ impl Interp {
1176 }
760 }
1177 Run::Step(State::Ret(v, (*cont).clone()))
761 Run::Step(State::Ret(v, (*cont).clone()))
1178 }
762 }
1179− Cont::InDef { cont, .. } => Run::Step(State::Ret(v, (*cont).clone())),
1180− Cont::MapNext {
1181− f,
1182− xs,
1183− idx,
1184− acc,
1185− cont,
1186− } => {
1187− acc.borrow_mut().push(v);
1188− let idx = idx + 1;
1189− if idx < xs.len() {
1190− let x = xs[idx].clone();
1191− let next = Cont::MapNext {
1192− f: f.clone(),
1193− xs,
1194− idx,
1195− acc,
1196− cont,
1197− };
1198− Run::Step(State::Apply(f, x, next))
1199− } else {
1200− Run::Step(State::Ret(Value::list(acc.take()), (*cont).clone()))
1201− }
1202− }
1203− Cont::FilterNext {
1204− p,
1205− xs,
1206− idx,
1207− acc,
1208− cont,
1209− } => {
1210− match v {
1211− Value::Bool(true) => acc.borrow_mut().push(xs[idx].clone()),
1212− Value::Bool(false) => (),
1213− v => {
1214− return Run::Crash(Crash::new(format!(
1215− "filter: predicate returned a {}",
1216− v.kind_name()
1217− )))
1218− }
1219− }
1220− let idx = idx + 1;
1221− if idx < xs.len() {
1222− let x = xs[idx].clone();
1223− let next = Cont::FilterNext {
1224− p: p.clone(),
1225− xs,
1226− idx,
1227− acc,
1228− cont,
1229− };
1230− Run::Step(State::Apply(p, x, next))
1231− } else {
1232− Run::Step(State::Ret(Value::list(acc.take()), (*cont).clone()))
1233− }
1234− }
1235− Cont::FoldlArg { f, xs, idx, cont } => {
1236− let x = xs[idx].clone();
1237− Run::Step(State::Apply(v, x, Cont::FoldlAcc { f, xs, idx, cont }))
1238− }
1239− Cont::FoldlAcc { f, xs, idx, cont } => {
1240− let idx = idx + 1;
1241− if idx < xs.len() {
1242− let next = Cont::FoldlArg {
1243− f: f.clone(),
1244− xs,
1245− idx,
1246− cont,
1247− };
1248− Run::Step(State::Apply(f, v, next))
1249− } else {
1250− Run::Step(State::Ret(v, (*cont).clone()))
1251− }
1252− }
1253− Cont::BuiltinResult {
1254− cname,
1255− check,
1256− def,
1257− cont,
1258− } => {
1259− if let Some(c) = &check {
1260− if let Err(cr) = c.check_result(&self.shapes, &cname, &v) {
1261− return Run::Crash(match &def {
1262− Some(d) => in_def(cr, d),
1263− None => cr,
1264− });
1265− }
1266− }
1267− Run::Step(State::Ret(v, (*cont).clone()))
1268− }
1269 }
763 }
1270 }
764 }
1271
765
@@ -1281,83 +775,40 @@ impl Interp {
1281 pending,
775 pending,
1282 } => {
776 } => {
1283 let mut all = args.clone();
777 let mut all = args.clone();
1284− let (cname, cexpr, named_at) = match pending {
778+ let (cname, cexpr) = match pending {
1285− Some((n, c, at)) => (n.clone(), Some(c.clone()), *at),
779+ Some((n, c)) => (n.clone(), Some(c.clone())),
1286− None => (name.clone(), None, 0),
780+ None => (name.clone(), None),
1287 };
781 };
782+ // compose nodes handle function arguments pointwise in
783+ // compose_apply; do not contract-check them here
784+ let skip_check = name == "(.)" && matches!(arg, Value::Fun(_));
1288 if let Some(c) = &cexpr {
785 if let Some(c) = &cexpr {
1289− // numbered by the signature, not by the arguments
786+ if !skip_check {
1290− // baked into a partial application (§4.13)
1291− let at = c.position().unwrap_or(args.len());
1292− if let Err(cr) = c.check_arg_at(&self.shapes, &cname, at, &arg) {
787+ if let Err(cr) = c.check_arg_at(&self.shapes, &cname, args.len(), &arg) {
1293− return Run::Crash(self.blame_builtin(cr, name, &cname));
788+ return Run::Crash(cr);
789+ }
1294 }
790 }
1295 }
791 }
1296 all.push(arg);
792 all.push(arg);
1297 let advanced = cexpr
793 let advanced = cexpr
1298 .map(|c| Rc::new(crate::shape::ContractExpr::apply_first_rc(c)));
794 .map(|c| Rc::new(crate::shape::ContractExpr::apply_first_rc(c)));
1299 if all.len() == *arity {
795 if all.len() == *arity {
1300− // a composition's contract derived from its operands
1301− // leaves results to theirs, checked as the
1302− // composition runs; a signature attached to it is
1303− // checked like any other
1304− let check = advanced.filter(|c| {
1305− name != "(.)"
1306− || matches!(**c, crate::shape::ContractExpr::Known { .. })
1307− });
1308− // a signed definition's builtin or composition that
1309− // consumes fewer arguments than its signature lists
1310− // (`addT = (+) . inc` under `Int -> Int -> Text`)
1311− // returns a function, which becomes the named partial
1312− // application `addT 1` under the rest of the
1313− // signature, as a lambda that runs out of parameters
1314− // first does (§4.13, §5.2)
1315− let k = match &check {
1316− Some(c)
1317− if !c.is_exhausted()
1318− && self.builtin_def(name, &cname).is_some() =>
1319− {
1320− // the arguments the definition was given, not
1321− // those baked into its value
1322− Cont::NamePartial {
1323− name: cname.clone(),
1324− cexpr: c.clone(),
1325− args: all[named_at.min(all.len())..].to_vec(),
1326− cont: Rc::new(k),
1327− }
1328− }
1329− _ => k,
1330− };
1331− if applies_functions(name) {
1332− // the result is checked once the functions have
1333− // run, and a crash while they run is blamed as
1334− // one in the builtin itself
1335− let def = self.builtin_def(name, &cname);
1336− let k = if check.is_some() || def.is_some() {
1337− Cont::BuiltinResult {
1338− cname: cname.clone(),
1339− check,
1340− def,
1341− cont: Rc::new(k),
1342− }
1343− } else {
1344− k
1345− };
1346− return match self.step_applying(name, &all, k) {
1347− Ok(run) => run,
1348− Err(c) => Run::Crash(self.blame_builtin(c, name, &cname)),
1349− };
1350− }
1351 match bf(self, &all) {
796 match bf(self, &all) {
1352 Ok(r) => {
797 Ok(r) => {
798+ // compose applications check results
799+ // incrementally as the composition runs
1353− if let Some(c) = &check {
800+ if let Some(c) = &advanced {
801+ if name != "(.)" {
802+ if let Err(cr) =
1354− if let Err(cr) = c.check_result(&self.shapes, &cname, &r) {
803+ c.check_result(&self.shapes, &cname, &r)
804+ {
1355− return Run::Crash(self.blame_builtin(cr, name, &cname));
805+ return Run::Crash(cr);
806+ }
1356 }
807 }
1357 }
808 }
1358 Run::Step(State::Ret(r, k))
809 Run::Step(State::Ret(r, k))
1359 }
810 }
1360− Err(c) => Run::Crash(self.blame_builtin(c, name, &cname)),
811+ Err(c) => Run::Crash(c),
1361 }
812 }
1362 } else {
813 } else {
1363 Run::Step(State::Ret(
814 Run::Step(State::Ret(
@@ -1366,7 +817,7 @@ impl Interp {
1366 arity: *arity,
817 arity: *arity,
1367 args: all,
818 args: all,
1368 f: *bf,
819 f: *bf,
1369− pending: advanced.map(|c| (cname, c, named_at)),
820+ pending: advanced.map(|c| (cname, c)),
1370 })),
821 })),
1371 k,
822 k,
1372 ))
823 ))
@@ -1377,25 +828,77 @@ impl Interp {
1377 params,
828 params,
1378 applied,
829 applied,
1379 applied_args,
830 applied_args,
831+ deferred,
1380 body,
832 body,
1381 env,
833 env,
1382 src,
834 src,
1383 pending,
835 pending,
1384 } => {
836 } => {
837+ if *deferred {
838+ // the body was deferred at the last application:
839+ // evaluate it now (catching crashes), then apply
840+ let msg = format!(
841+ "{}: body crashed when applied",
842+ name.clone().unwrap_or_default()
843+ );
844+ return Run::Step(State::Eval(
845+ body.clone(),
846+ env.clone(),
847+ Cont::Try {
848+ on_ok: Some(TryOk::DeferredBody {
849+ arg,
850+ cont: Rc::new(k),
851+ }),
852+ snapshot: *self.fresh.borrow(),
853+ on_crash: Box::new(State::Ret(
854+ Value::Bool(false),
855+ Cont::CrashWith(msg),
856+ )),
857+ },
858+ ));
859+ }
1385 let (cname, cexpr) = match pending {
860 let (cname, cexpr) = match pending {
1386 Some((n, c)) => (Some(n.clone()), Some(c.clone())),
861 Some((n, c)) => (Some(n.clone()), Some(c.clone())),
1387 None => (name.clone(), None),
862 None => (name.clone(), None),
1388 };
863 };
1389− // a function argument is checked like any other: where
864+ // A signed definition's application to a function value is
1390− // the signature wants a Repo, `new new` is a contract
865+ // tried as a plain application first; if that crashes (as
1391− // crash, not a composition (§4.13, §4.9)
866+ // `abandon (contract everything)` does at load time, since
867+ // the definition is a Repo-level function), it is function
868+ // composition instead (§4.1 note: the composition is a
869+ // value before it is applied to the repository).
870+ if *applied == 0 && matches!(arg, Value::Fun(_)) && pending.is_some() {
871+ // probe: contract-check the argument; if it fails, the
872+ // application is composition instead (§4.1 note)
873+ let probe_ok = match &cexpr {
874+ Some(c) => {
875+ let r = c.check_arg(&self.shapes, "", &arg).is_ok();
876+ r
877+ }
878+ None => true,
879+ };
880+ if !probe_ok {
881+ let f = Value::Fun(Rc::new(FunVal::Closure {
882+ name: name.clone(),
883+ params: params.clone(),
884+ applied: *applied,
885+ applied_args: applied_args.clone(),
886+ deferred: false,
887+ body: body.clone(),
888+ env: env.clone(),
889+ src: src.clone(),
890+ pending: pending.clone(),
891+ }));
892+ return match crate::builtins::compose_values(self, f, arg) {
893+ Ok(composed) => Run::Step(State::Ret(composed, k)),
894+ Err(cr) => Run::Crash(cr),
895+ };
896+ }
897+ }
1392 if let Some(c) = &cexpr {
898 if let Some(c) = &cexpr {
1393 let fname = cname.clone().unwrap_or_default();
899 let fname = cname.clone().unwrap_or_default();
1394− let at = c.position().unwrap_or(applied_args.len());
1395− if let Err(cr) = c.check_arg_at(&self.shapes, &fname, at, &arg) {
900+ if let Err(cr) = c.check_arg_at(&self.shapes, &fname, applied_args.len(), &arg) {
1396− // a violation is in the definition checked
1397− // (§1.4, §4.13's `describe 3`)
1398− return Run::Crash(in_def(cr, &fname));
901+ return Run::Crash(cr);
1399 }
902 }
1400 }
903 }
1401 let p = &params[*applied];
904 let p = &params[*applied];
@@ -1406,78 +909,47 @@ impl Interp {
1406 let new_applied = applied + 1;
909 let new_applied = applied + 1;
1407 let advanced = cexpr
910 let advanced = cexpr
1408 .map(|c| Rc::new(crate::shape::ContractExpr::apply_first_rc(c)));
911 .map(|c| Rc::new(crate::shape::ContractExpr::apply_first_rc(c)));
912+ // a signed definition's body is not evaluated until its
913+ // contract is exhausted, so partial applications of named
914+ // definitions stay named values (§5.2's `describe "wip"`)
1409 let contract_exhausted = match &advanced {
915 let contract_exhausted = match &advanced {
1410 Some(c) => c.is_exhausted(),
916 Some(c) => c.is_exhausted(),
1411 None => true,
917 None => true,
1412 };
918 };
1413− if new_applied == params.len() {
919+ if new_applied == params.len() && contract_exhausted {
1414− // the definition whose lambda this body is, if any,
920+ Run::Step(State::Eval(
1415− // executes while it runs (§1.4)
921+ body.clone(),
1416− let def = self
922+ env2,
1417− .def_bodies
1418− .get(&Rc::as_ptr(body))
1419− .map(|(d, _)| d.clone());
1420− // a call in tail position of a definition's body
1421− // leaves that definition nothing to do but pass this
1422− // body's value on, so its frame gives way to the one
1423− // this body runs under, and unsigned tail recursion
1424− // keeps its continuation constant in size
1425− let signed = cname.is_some() && advanced.is_some();
1426− let k = match k {
1427− Cont::InDef { cont, .. } if signed || def.is_some() => {
1428− (*cont).clone()
1429− }
1430− k => k,
1431− };
1432− // the body is evaluated now, as at any application
1433− // (§4.1): a signature adds checks and never changes
1434− // when a body runs, or whether an `or` around the
1435− // application catches its crash (§4.6, §4.13)
1436− let k = match (cname, advanced) {
923+ match (&cname, &advanced) {
1437− // a tail call of the signed definition whose
924+ (Some(n), Some(c)) => Cont::CheckExpr {
1438− // body `k` continues: its frames serve this one
925+ name: n.clone(),
1439− (Some(n), Some(c)) if checks_already(&k, &n, &c) => k,
926+ cexpr: c.clone(),
1440− (Some(n), Some(c)) => {
1441− // a lambda that runs out of parameters before
1442− // its signature (`describe = \m -> mapRoot …`)
1443− // returns a function, which becomes the named
1444− // partial application `describe "wip"` (§5.2)
1445− // under the rest of the signature
1446− let k = if c.is_exhausted() {
1447− k
1448− } else {
1449− let mut args = applied_args.clone();
1450− args.push(arg);
1451− Cont::NamePartial {
1452− name: n.clone(),
1453− cexpr: c.clone(),
1454− args,
1455− cont: Rc::new(k),
1456− }
1457− };
1458− Cont::CheckExpr {
1459− name: n,
1460− cexpr: c,
1461− cont: Rc::new(k),
1462− }
1463− }
1464− _ => k,
1465− };
1466− // inside the check of the definition it was bound to,
1467− // which names this one already when it is the same
1468− let k = match def {
1469− Some(d)
1470− if !matches!(&k, Cont::CheckExpr { name, .. }
1471− if **name == *d) =>
1472− {
1473− Cont::InDef {
1474− name: d,
1475 cont: Rc::new(k),
927 cont: Rc::new(k),
1476− }
928+ },
929+ _ => k,
1477− }
930+ },
1478− _ => k,
931+ ))
932+ } else if new_applied == params.len() {
933+ // the lambda is exhausted but the contract is not:
934+ // defer the body so the definition stays a named
935+ // partial application (§5.2)
936+ let mut new_args = applied_args.clone();
937+ new_args.push(arg.clone());
938+ let v = Value::Fun(Rc::new(FunVal::Closure {
939+ name: name.clone(),
940+ params: params.clone(),
941+ applied: new_applied,
942+ applied_args: new_args,
943+ deferred: true,
944+ body: body.clone(),
945+ env: env2,
946+ src: src.clone(),
947+ pending: match (&cname, &advanced) {
948+ (Some(n), Some(c)) => Some((n.clone(), c.clone())),
949+ _ => None,
950+ },
1479− };
951+ }));
1480− Run::Step(State::Eval(body.clone(), env2, k))
952+ Run::Step(State::Ret(v, k))
1481 } else if contract_exhausted {
953 } else if contract_exhausted {
1482 // lambda params remain but the contract is satisfied;
954 // lambda params remain but the contract is satisfied;
1483 // keep currying (unsigned tail)
955 // keep currying (unsigned tail)
@@ -1488,6 +960,7 @@ impl Interp {
1488 params: params.clone(),
960 params: params.clone(),
1489 applied: new_applied,
961 applied: new_applied,
1490 applied_args: new_args,
962 applied_args: new_args,
963+ deferred: false,
1491 body: body.clone(),
964 body: body.clone(),
1492 env: env2,
965 env: env2,
1493 src: src.clone(),
966 src: src.clone(),
@@ -1505,6 +978,7 @@ impl Interp {
1505 params: params.clone(),
978 params: params.clone(),
1506 applied: new_applied,
979 applied: new_applied,
1507 applied_args: new_args,
980 applied_args: new_args,
981+ deferred: false,
1508 body: body.clone(),
982 body: body.clone(),
1509 env: env2,
983 env: env2,
1510 src: src.clone(),
984 src: src.clone(),
@@ -1517,10 +991,34 @@ impl Interp {
1517 }
991 }
1518 }
992 }
1519 FunVal::ComposeLazy(f, g) => {
993 FunVal::ComposeLazy(f, g) => {
1520− // (f . g) x = f (g x) (§4.9), whatever x is: apply g,
994+ let f = f.clone();
995+ let g = g.clone();
996+ if matches!(arg, Value::Fun(_)) {
997+ // pointwise: (f . g) h = f (g h) — but g h is not
998+ // evaluated yet either; compose lazily
999+ let inner = Value::Fun(Rc::new(FunVal::ComposeLazy(g, arg)));
1000+ Run::Step(State::Ret(
1001+ Value::Fun(Rc::new(FunVal::ComposeLazy(f, inner))),
1002+ k,
1003+ ))
1004+ } else {
1521− // then f to the result; a crash in either keeps its
1005+ // apply g, catching crashes, then apply f to the result
1522− // message and unwinds to any enclosing `or` (§4.7)
1523− Run::Step(State::Apply(g.clone(), arg, Cont::Fun(f.clone(), Rc::new(k))))
1006+ Run::Step(State::Apply(
1007+ g,
1008+ arg,
1009+ Cont::Try {
1010+ on_ok: Some(TryOk::ComposeInner {
1011+ f,
1012+ cont: Rc::new(k),
1013+ }),
1014+ snapshot: *self.fresh.borrow(),
1015+ on_crash: Box::new(State::Ret(
1016+ Value::Bool(false),
1017+ Cont::CrashWith("composition crashed".to_string()),
1018+ )),
1019+ },
1020+ ))
1021+ }
1524 }
1022 }
1525 FunVal::OrFun(a, b) => {
1023 FunVal::OrFun(a, b) => {
1526 // (f or g) x = f x or g x
1024 // (f or g) x = f x or g x
@@ -1531,11 +1029,7 @@ impl Interp {
1531 a,
1029 a,
1532 arg.clone(),
1030 arg.clone(),
1533 Cont::Try {
1031 Cont::Try {
1534− on_ok: Some(TryOk::OrFunLhs {
1032+ on_ok: Some(TryOk::OrFunLhs { cont: k.clone() }),
1535− rhs: b.clone(),
1536− arg: arg.clone(),
1537− cont: k.clone(),
1538− }),
1539 snapshot: *self.fresh.borrow(),
1033 snapshot: *self.fresh.borrow(),
1540 on_crash: Box::new(State::Apply(b, arg, Cont::Shared(k))),
1034 on_crash: Box::new(State::Apply(b, arg, Cont::Shared(k))),
1541 },
1035 },
@@ -1547,7 +1041,7 @@ impl Interp {
1547 Run::Step(State::Apply(
1041 Run::Step(State::Apply(
1548 labelled.clone(),
1042 labelled.clone(),
1549 text,
1043 text,
1550− Cont::ApplyTo(arg, Rc::new(k)),
1044+ Cont::LabelledArg(arg, Rc::new(k)),
1551 ))
1045 ))
1552 }
1046 }
1553 },
1047 },
src/help.rsdeleted−668
@@ -1,668 +0,0 @@
1−//! Read-only command discovery, built from the user's unevaluated config.
2−
3−use j::ast::{Item, TypeExpr};
4−use j::config;
5−use j::lex::{lex, Tok};
6−use j::parse::parse_config;
7−use std::collections::BTreeMap;
8−use std::rc::Rc;
9−
10−pub const OVERVIEW: &str = r#"j — a functional CLI for Jujutsu repositories
11−
12−Usage:
13− j EXPRESSION… Evaluate an expression
14− printf '%s\n' 'tree' | j Read an expression from stdin
15− j help [NAME] Explain a command or config name
16− j commands [QUERY] Find everyday commands by task
17− j commands all Browse the complete vocabulary
18− j help config Set your identity or repair configuration
19−
20−Start a repository:
21− j setup 'Ada Lovelace' ada@example.org Create config with your identity
22− j init Create a repository in the current directory
23− j clone URL [DIR] Clone a repository
24−
25−Inspect (default config):
26− j tree Show the history and current focus
27− j status Show the focused commit and changed paths
28− j review Show changes (unified diff if difft is absent)
29− j log List commits
30−
31−Edit (default config):
32− j save Record current file edits
33− j 'describe "add a feature"' Set the focused commit's message
34− j new Start an empty child and move to it
35− j prev Move to the parent commit
36− j 'new . goto trunk' Start a new commit on the main line
37− j squash Fold the focus into its parent
38− j 'rebase trunk' Move the focused subtree onto the main line
39−
40−Preview and recover:
41− j 'preview squash' Check and preview a squash without saving
42− j undo Undo the last operation
43− j redo Reapply an undone operation
44− j ops Show the operation log
45−
46−Sync:
47− j remote URL Set the origin remote
48− j fetch Fetch from origin
49− j 'push (label "feature" here)' Publish the focus as a remote bookmark
50− j push relabel Update existing remote bookmarks
51−
52−Push sends recorded commits. After editing files, run `j save` to record them
53−before pushing; `j status` and other inspections do not save those edits.
54−
55−Expressions compose right to left: `describe "wip" . new` runs new first.
56−A function result receives the current repository. A Repo result saves the
57−edit; other results print. Quote the whole expression with shell single
58−quotes to preserve text's double quotes and operators such as *, <, and >.
59−
60−Your config.j defines the vocabulary: edits (Repo -> Repo), inspections,
61−revsets, filesets, types, and custom names. Search with `j commands rebase`
62−or `j commands Edit`; use `j help squash` or `j help commands` for details.
63−"#;
64−
65−pub const CONFIG_HELP: &str = r#"Configuration and identity
66−
67−For a new installation, create the complete config with your identity:
68−
69− j setup 'Ada Lovelace' ada@example.org
70−
71−Use your own name and email. Then run `j init` or `j clone URL`.
72−To create the complete default and edit identity yourself, run `j setup`.
73−Setup never replaces an existing config.
74−
75−If the file already exists, edit its existing `user` definition:
76−
77− user = { name = "Ada Lovelace", email = "ada@example.org" }
78−
79−Keep the rest of the file: it defines your commands. The example above is
80−one definition, not a complete config. Set both fields before saving changes.
81−The shipped "Your Name" / "you@example.com" placeholders cannot author commits.
82−An existing configuration is never overwritten or upgraded automatically.
83−
84−If configuration is broken, repair the reported line in this file. General
85−help and reserved-command help remain available without loading config.
86−`j commands` checks the vocabulary without evaluating it or changing a repo.
87−
88−Configuration lives at $XDG_CONFIG_HOME/j/config.j when XDG_CONFIG_HOME is
89−set, otherwise at ~/.config/j/config.j. `j setup` creates an editable default;
90−help never creates or changes files.
91−"#;
92−
93−struct Reserved {
94− name: &'static str,
95− usage: &'static str,
96− summary: &'static str,
97− detail: &'static str,
98−}
99−
100−const RESERVED: &[Reserved] = &[
101− Reserved {
102− name: "clone",
103− usage: "j clone URL [DIR]",
104− summary: "Clone a repository.",
105− detail: "Clone URL into DIR, or into a directory named after the repository.\nFor first-time setup, run `j setup NAME EMAIL`. Shell-quote paths containing spaces.\nCloning does not change your shell's directory; enter the new folder afterward.\n\nExamples:\n j clone https://example.com/team/project.git project\n cd project\n j status\n\n j clone '/path/to/source repo' 'my project'\n cd 'my project'\n",
106− },
107− Reserved {
108− name: "commands",
109− usage: "j commands [QUERY]",
110− summary: "Find everyday commands by task, or browse the full vocabulary.",
111− detail: "Without a query, show common tasks and a few custom definitions.\n`j commands all` lists every configured definition, builtin, operator, and type.\nSearch by task (save, record, commit, branch, checkout), name, signature, or\nsource comments. Search ignores case and puts direct matches and everyday\ncommands first. Use `j help NAME` for examples and types.\n\nThe config is parsed and validated without evaluating definitions or opening\na repository. No config file is created; when absent, defaults are shown.\n\nExamples:\n j commands\n j commands save\n j commands rebase\n j commands Edit\n j commands all\n",
112− },
113− Reserved {
114− name: "fetch",
115− usage: "j fetch",
116− summary: "Fetch commits and bookmarks from origin.",
117− detail: "Fetch from the origin remote and import its commits and bookmarks.\nSet the remote with `j remote URL`.\n\nExample:\n j fetch\n",
118− },
119− Reserved {
120− name: "help",
121− usage: "j help [NAME]",
122− summary: "Show getting-started help or explain a name.",
123− detail: "With no NAME, show common commands and the expression model. With NAME,\nshow CLI usage or a config name's signature, documentation, and source line.\nConfig help reads declarations without evaluating them or opening a repository.\n\nExamples:\n j help\n j help push\n j help squash\n j help Edit\n j help '(.)'\n\nUse `j commands QUERY` to find names by purpose or signature.\n",
124− },
125− Reserved {
126− name: "init",
127− usage: "j init",
128− summary: "Create a repository in the current directory.",
129− detail: "Create a colocated .jj/.git repository and a working-copy commit. An\nexisting .git repository is adopted. On a new installation, first run\n`j setup NAME EMAIL`; use `j help config` to update an existing identity.\nThe shipped placeholder identity cannot author commits.\n\nExample:\n j setup 'Ada Lovelace' ada@example.org\n mkdir project\n cd project\n j init\n",
130− },
131− Reserved {
132− name: "ops",
133− usage: "j ops",
134− summary: "Show the operation log.",
135− detail: "Print the operation history, newest first, with the current operation\nmarked by *. Undo and redo operations are labelled. This reads repository\nhistory without loading config.j.\n\nExample:\n j ops\n\nRelated: j help undo, j help redo\n",
136− },
137− Reserved {
138− name: "push",
139− usage: "j push EXPRESSION",
140− summary: "Publish or delete bookmarks on origin.",
141− detail: "Evaluate EXPRESSION to a list of Push and Drop records, then update origin.\nA function result receives the recorded repository; working-copy changes\nare not snapshotted by push. Unrecorded edits produce a warning before sending.\nRun `j id` to record them, then push again to include them; `j status` does not\nrecord edits. The default config provides label, unlabel, rename, and relabel.\n\nExamples (default config):\n j 'push (label \"feature\" here)'\n j push relabel\n j 'push (unlabel \"feature\")'\n j 'push (rename \"old\" \"new\")'\n\nThe unlabel example deletes the remote bookmark.\n",
142− },
143− Reserved {
144− name: "redo",
145− usage: "j redo",
146− summary: "Reapply an undone operation.",
147− detail: "Reapply the operation removed by undo. Refuses if there is nothing to redo\nor if restoring the working copy would overwrite unsnapshotted changes.\nRecording new edits with `j id` creates an operation and clears pending redos.\nInspect `j ops` before choosing how to preserve or discard those edits.\n\nExample:\n j redo\n\nRelated: j help undo, j help ops\n",
148− },
149− Reserved {
150− name: "remote",
151− usage: "j remote URL",
152− summary: "Set the origin remote URL.",
153− detail: "Add origin or replace its URL. Fetch and push use this remote.\n\nExample:\n j remote https://example.com/team/project.git\n j fetch\n",
154− },
155− Reserved {
156− name: "setup",
157− usage: "j setup [NAME EMAIL]",
158− summary: "Create your complete config before starting a repository.",
159− detail: "With NAME and EMAIL, create the full default config with your identity.\nWithout arguments, create the default and print the exact definition to edit.\nAn existing config is never overwritten: edit its `user` definition directly.\nSetup does not create or open a repository.\n\nExamples:\n j setup 'Ada Lovelace' ada@example.org\n j init\n\n j setup\n j help config\n",
160− },
161− Reserved {
162− name: "undo",
163− usage: "j undo",
164− summary: "Undo the last operation.",
165− detail: "Restore the repository state before the last operation. Repeat to walk\nback through history. Refuses if restoring the working copy would overwrite\nunsnapshotted changes. `j id` preserves edits as a new operation: the next\nundo reverses that recording before reaching the earlier operation. Use\n`j ops` to inspect history; `j redo` can restore the undone recording.\nUndo does not reverse changes already sent to a remote.\n\nExample:\n j undo\n\nRelated: j help redo, j help ops\n",
166− },
167−];
168−
169−pub fn reserved_help(name: &str) -> Option<String> {
170− RESERVED
171− .iter()
172− .find(|cmd| cmd.name == name)
173− .map(|cmd| format!("Usage: {}\n\n{}\n\n{}", cmd.usage, cmd.summary, cmd.detail))
174−}
175−
176−#[derive(Clone, Copy)]
177−enum Kind {
178− Definition,
179− Builtin,
180− Type,
181−}
182−
183−struct Entry {
184− name: String,
185− kind: Kind,
186− signature: Option<TypeExpr>,
187− line: usize,
188− docs: Vec<String>,
189−}
190−
191−impl Entry {
192− fn declaration(&self) -> String {
193− let name = if config::is_reserved_builtin(&self.name)
194− && !self.name.chars().next().unwrap_or('_').is_alphabetic()
195− {
196− format!("({})", self.name)
197− } else {
198− self.name.clone()
199− };
200− match (&self.signature, self.kind) {
201− (Some(ty), Kind::Type) => format!("{} = {}", name, type_text(ty)),
202− (Some(ty), _) => format!("{} : {}", name, type_text(ty)),
203− (None, _) => format!("{} (definition; no signature)", name),
204− }
205− }
206−}
207−
208−struct Task {
209− group: &'static str,
210− name: &'static str,
211− usage: &'static str,
212− summary: &'static str,
213−}
214−
215−const TASKS: &[Task] = &[
216− Task { group: "Start", name: "setup", usage: "j setup NAME EMAIL", summary: "Set your name and email." },
217− Task { group: "Start", name: "init", usage: "j init", summary: "Create a repository here." },
218− Task { group: "Start", name: "clone", usage: "j clone URL [DIR]", summary: "Clone a repository, then cd into it." },
219− Task { group: "Inspect", name: "status", usage: "j status", summary: "See your current change and files." },
220− Task { group: "Inspect", name: "tree", usage: "j tree", summary: "See history and your current commit." },
221− Task { group: "Inspect", name: "review", usage: "j review", summary: "Review changed file contents." },
222− Task { group: "Work", name: "save", usage: "j save", summary: "Record current file edits." },
223− Task { group: "Work", name: "id", usage: "j id", summary: "Record current file edits." },
224− Task { group: "Work", name: "describe", usage: "j 'describe \"message\"'", summary: "Name the current commit." },
225− Task { group: "Work", name: "new", usage: "j new", summary: "Start the next commit before editing." },
226− Task { group: "Work", name: "preview", usage: "j 'preview squash'", summary: "Check an edit without saving it." },
227− Task { group: "Work", name: "prev", usage: "j prev", summary: "Move to the parent commit." },
228− Task { group: "Share", name: "remote", usage: "j remote URL", summary: "Set your remote repository." },
229− Task { group: "Share", name: "fetch", usage: "j fetch", summary: "Get remote commits and bookmarks." },
230− Task { group: "Share", name: "push", usage: "j help push", summary: "Learn how to publish a bookmark." },
231− Task { group: "Recover", name: "undo", usage: "j undo", summary: "Undo the last operation." },
232− Task { group: "Recover", name: "redo", usage: "j redo", summary: "Reapply an undone operation." },
233− Task { group: "Recover", name: "ops", usage: "j ops", summary: "Inspect the operation history." },
234−];
235−
236−fn everyday_list(entries: &[Entry]) -> String {
237− let mut out = String::from("Everyday commands:\n");
238− let reference = config::load_config(include_str!("../config.j")).expect("embedded config is valid");
239− let mut group = "";
240− for task in TASKS {
241− let entry = entries.iter().find(|entry| entry.name == task.name);
242− let reserved = RESERVED.iter().any(|command| command.name == task.name);
243− if !reserved && entry.is_none() {
244− continue;
245− }
246− if task.name == "id" && entries.iter().any(|entry| entry.name == "save") {
247− continue;
248− }
249− if task.group != group {
250− out.push_str(&format!("\n{}:\n", task.group));
251− group = task.group;
252− }
253− let custom_summary;
254− let summary = match entry {
255− Some(entry) if !entry.docs.is_empty() => entry.docs[0].as_str(),
256− Some(entry) if entry.signature.is_none() && entry.docs.is_empty() => {
257− custom_summary = format!("Custom definition; see `j help {}`.", entry.name);
258− custom_summary.as_str()
259− }
260− _ => task.summary,
261− };
262− let changed_signature = entry.is_some_and(|entry| entry.signature.as_ref() != reference.sigs.get(task.name));
263− let usage = if changed_signature || (task.name == "preview" && !entries.iter().any(|entry| entry.name == "squash")) {
264− format!("j help {}", task.name)
265− } else {
266− task.usage.to_string()
267− };
268− out.push_str(&format!(" {:<29} {}\n", usage, truncate(summary, 47)));
269− }
270− // Surface a few additions without letting a large custom vocabulary turn
271− // the starting guide into the complete catalog again.
272− let custom: Vec<_> = entries.iter().filter(|entry| {
273− matches!(entry.kind, Kind::Definition)
274− && !reference.defs.iter().any(|(name, _)| name == &entry.name)
275− }).collect();
276− if !custom.is_empty() {
277− out.push_str("\nYour custom definitions:\n");
278− for entry in custom.iter().take(6) {
279− let docs = entry.docs.first().map(String::as_str).unwrap_or("Use j help for details.");
280− out.push_str(&format!(" {:<29} {}\n", entry.name, truncate(docs, 55)));
281− }
282− if custom.len() > 6 {
283− out.push_str(" More custom definitions are listed by `j commands all`.\n");
284− }
285− }
286− out.push_str("\nUse `j help NAME` for examples. Search by task: `j commands save`.\nUse `j commands all` for every definition, builtin, operator, and type.\n");
287− out
288−}
289−
290−fn intent_names(query: &str) -> &'static [&'static str] {
291− match query {
292− "save" | "record" | "snapshot" | "save changes" | "record changes" => &["save", "id"],
293− "commit" | "git commit" => &["describe", "new", "save", "id"],
294− "branch" | "git branch" => &["new", "label", "push", "goto"],
295− "checkout" | "switch" | "switch branch" | "git checkout" => &["goto", "prev", "next"],
296− "diff" | "changes" | "git diff" => &["review", "status", "changed", "diff"],
297− _ => &[],
298− }
299−}
300−
301−pub fn command_list(src: &str, query: &str) -> Result<String, String> {
302− let entries = entries(src)?;
303− let query = query.trim().to_lowercase();
304− if query.is_empty() {
305− return Ok(everyday_list(&entries));
306− }
307− let all = query == "all";
308− let intents = intent_names(&query);
309− let matches = |text: &str| all || text.to_lowercase().contains(&query);
310− let mut results = Vec::new();
311− for cmd in RESERVED {
312− let intent = intents.iter().position(|name| *name == cmd.name);
313− if matches(&format!("{} {} {}", cmd.usage, cmd.summary, cmd.detail)) || intent.is_some() {
314− let rank = if all || query == cmd.name { 0 } else { intent.map(|i| i + 1).unwrap_or(40) };
315− results.push((rank, cmd.name.to_string(), format!(" {:<24} {}\n", cmd.usage, cmd.summary)));
316− }
317− }
318− for entry in &entries {
319− let declaration = entry.declaration();
320− let intent = intents.iter().position(|name| *name == entry.name);
321− if !matches(&format!("{} {}", declaration, entry.docs.join(" "))) && intent.is_none() {
322− continue;
323− }
324− let everyday = TASKS.iter().any(|task| task.name == entry.name);
325− let exact = query == entry.name.to_lowercase() && (!matches!(entry.kind, Kind::Type) || intents.is_empty());
326− let rank = if all { 1 } else if exact { 0 }
327− else { intent.map(|i| i + 1).unwrap_or(if everyday { 20 } else { 30 }) };
328− let mut description = format!(" {}\n", declaration);
329− if let Some(doc) = entry.docs.first() {
330− description.push_str(&format!(" {}\n", truncate(doc, 110)));
331− }
332− results.push((rank, entry.name.clone(), description));
333− }
334− results.sort_by(|a, b| (&a.0, a.1.to_lowercase(), &a.1).cmp(&(&b.0, b.1.to_lowercase(), &b.1)));
335− let mut out = String::new();
336− if matches!(query.as_str(), "stash" | "git stash") && !entries.iter().any(|entry| entry.name == "stash") {
337− out.push_str("There is no direct stash command in this config.\n");
338− let save = if entries.iter().any(|entry| entry.name == "save") { Some("save") }
339− else if entries.iter().any(|entry| entry.name == "id") { Some("id") } else { None };
340− if let Some(save) = save {
341− out.push_str(&format!("`j {}` records file edits in the current commit.\n", save));
342− }
343− if entries.iter().any(|entry| entry.name == "new") {
344− out.push_str("`j new` starts a child commit; the recorded work remains in its parent.\n");
345− }
346− out.push_str("\n");
347− }
348− if results.is_empty() {
349− out.push_str("No matching commands or config names. Try `j commands` for common tasks or `j commands all` for every name.\n");
350− } else {
351− out.push_str(if all { "All commands and config vocabulary:\n" } else { "Matching commands and config names:\n" });
352− for (_, _, description) in results {
353− out.push_str(&description);
354− }
355− out.push_str("\nUse `j help NAME` for examples and details; `j commands QUERY` to search.\n");
356− }
357− Ok(out)
358−}
359−
360−pub fn command_help(src: &str, name: &str) -> Result<Option<String>, String> {
361− let name = name.trim();
362− let name = name
363− .strip_prefix('(')
364− .and_then(|n| n.strip_suffix(')'))
365− .unwrap_or(name)
366− .trim();
367− let entry = entries(src)?.into_iter().find(|entry| entry.name == name);
368− Ok(entry.map(|entry| {
369− let kind = match entry.kind {
370− Kind::Definition => "Config definition",
371− Kind::Builtin => "Builtin declared in config",
372− Kind::Type => "Type declared in config",
373− };
374− let mut out = String::new();
375− if !entry.docs.is_empty() {
376− for doc in entry.docs.iter().take(24) {
377− out.push_str(doc);
378− out.push('\n');
379− }
380− if entry.docs.len() > 24 {
381− out.push_str("… See the source for the remaining comments.\n");
382− }
383− out.push('\n');
384− }
385− out.push_str(&format!("{}\n\n{}\nSource: line {}\n", entry.declaration(), kind, entry.line));
386− out
387− }))
388−}
389−
390−fn entries(src: &str) -> Result<Vec<Entry>, String> {
391− let cfg = config::load_config(src).map_err(|err| err.message())?;
392− let items = parse_config(src, Rc::new(config::reserved_set()))
393− .map_err(|err| format!("line {}: {}", err.line, err.msg))?;
394− let tokens = lex(src).map_err(|err| format!("line {}: {}", err.line, err.msg))?;
395− let starts: Vec<_> = tokens
396− .iter()
397− .enumerate()
398− .filter(|(_, token)| token.col == 1 && !matches!(token.tok, Tok::Newline | Tok::Eof))
399− .map(|(index, _)| index)
400− .collect();
401− let comments = comments(src, &tokens);
402− let mut entries = BTreeMap::<String, Entry>::new();
403− for (index, item) in items.into_iter().enumerate() {
404− let first = &tokens[starts[index]];
405− let end = starts.get(index + 1).copied().unwrap_or(tokens.len());
406− let last = tokens[starts[index]..end]
407− .iter()
408− .rev()
409− .find(|token| !matches!(token.tok, Tok::Newline | Tok::Eof))
410− .unwrap();
411− let next_start = starts
412− .get(index + 1)
413− .map(|&i| tokens[i].start)
414− .unwrap_or(src.len());
415− let docs = item_docs(src, first.line, last.end, next_start, &comments);
416− let (name, signature, kind) = match item {
417− Item::TypeDecl(name, ty) => (name, Some(ty), Kind::Type),
418− Item::Signature(name, ty, _) => {
419− let kind = if cfg.builtin_names.contains(&name) {
420− Kind::Builtin
421− } else {
422− Kind::Definition
423− };
424− (name, Some(ty), kind)
425− }
426− Item::Definition(name, _, _) => (name, None, Kind::Definition),
427− };
428− let entry = entries.entry(name.clone()).or_insert(Entry {
429− name,
430− signature,
431− kind,
432− line: first.line,
433− docs: Vec::new(),
434− });
435− for doc in docs {
436− if !entry.docs.contains(&doc) {
437− entry.docs.push(doc);
438− }
439− }
440− }
441− let mut entries: Vec<_> = entries.into_values().collect();
442− entries.sort_by_cached_key(|entry| (entry.name.to_lowercase(), entry.name.clone()));
443− Ok(entries)
444−}
445−
446−fn type_text(ty: &TypeExpr) -> String {
447− match ty {
448− TypeExpr::Con(name) => name.clone(),
449− TypeExpr::List(item) => format!("[{}]", type_text(item)),
450− TypeExpr::Record(fields) => format!(
451− "{{ {} }}",
452− fields
453− .iter()
454− .map(|(name, ty)| format!("{} : {}", name, type_text(ty)))
455− .collect::<Vec<_>>()
456− .join(", ")
457− ),
458− TypeExpr::Fun(arg, result) => {
459− let arg_text = type_text(arg);
460− let arg_text = if matches!(arg.as_ref(), TypeExpr::Fun(_, _)) {
461− format!("({})", arg_text)
462− } else {
463− arg_text
464− };
465− format!("{} -> {}", arg_text, type_text(result))
466− }
467− }
468−}
469−
470−fn truncate(text: &str, length: usize) -> String {
471− let mut chars = text.chars();
472− let prefix: String = chars.by_ref().take(length).collect();
473− if chars.next().is_some() {
474− format!("{}…", prefix)
475− } else {
476− prefix
477− }
478−}
479−
480−struct Comment {
481− start: usize,
482− first_line: usize,
483− last_line: usize,
484− standalone: bool,
485− continuation: bool,
486− text: Vec<String>,
487−}
488−
489−/// Comments occupy gaps between lexer tokens, so text such as "-- not a doc"
490−/// inside string literals cannot be mistaken for documentation.
491−fn comments(src: &str, tokens: &[j::lex::SpTok]) -> Vec<Comment> {
492− let mut comments = Vec::<Comment>::new();
493− let mut cursor = 0;
494− for token in tokens
495− .iter()
496− .filter(|token| !matches!(token.tok, Tok::Newline))
497− {
498− let mut rest = &src[cursor..token.start];
499− while !rest.trim_start().is_empty() {
500− rest = rest.trim_start();
501− let start = token.start - rest.len();
502− let (raw, consumed) = if let Some(body) = rest.strip_prefix("--") {
503− let length = body.find('\n').unwrap_or(body.len());
504− (&body[..length], length + 2)
505− } else if rest.starts_with("{-") {
506− let mut depth = 1;
507− let mut offset = 2;
508− while depth > 0 {
509− if rest[offset..].starts_with("{-") {
510− depth += 1;
511− offset += 2;
512− } else if rest[offset..].starts_with("-}") {
513− depth -= 1;
514− offset += 2;
515− } else {
516− offset += rest[offset..].chars().next().unwrap().len_utf8();
517− }
518− }
519− (&rest[2..offset - 2], offset)
520− } else {
521− break;
522− };
523− let first_line = src[..start].bytes().filter(|&b| b == b'\n').count() + 1;
524− let last_line = first_line + rest[..consumed].bytes().filter(|&b| b == b'\n').count();
525− let line_start = src[..start].rfind('\n').map_or(0, |i| i + 1);
526− let standalone = src[line_start..start].trim().is_empty();
527− let continuation = standalone
528− && start > line_start
529− && comments.last().is_some_and(|previous| {
530− previous.last_line + 1 == first_line
531− && (!previous.standalone || previous.continuation)
532− });
533− comments.push(Comment {
534− start,
535− first_line,
536− last_line,
537− standalone,
538− continuation,
539− text: raw.lines().map(|line| line.trim().to_string()).collect(),
540− });
541− rest = &rest[consumed..];
542− }
543− cursor = token.end;
544− }
545− comments
546−}
547−
548−fn item_docs(
549− src: &str,
550− first_line: usize,
551− end: usize,
552− next_start: usize,
553− comments: &[Comment],
554−) -> Vec<String> {
555− let mut preceding = Vec::new();
556− let mut line = first_line.saturating_sub(1);
557− while let Some(comment) = comments
558− .iter()
559− .rev()
560− .find(|comment| comment.last_line == line && comment.standalone && !comment.continuation)
561− {
562− if comment
563− .text
564− .iter()
565− .all(|text| text.chars().all(|c| "-=_ ".contains(c)))
566− {
567− break;
568− }
569− preceding.push(comment);
570− line = comment.first_line.saturating_sub(1);
571− }
572− preceding.reverse();
573− let last_line = src[..end].bytes().filter(|&b| b == b'\n').count() + 1;
574− let inline = comments.iter().filter(|comment| {
575− comment.first_line >= first_line
576− && comment.start < next_start
577− && (comment.first_line <= last_line || comment.continuation)
578− });
579− preceding
580− .into_iter()
581− .chain(inline)
582− .flat_map(|comment| comment.text.iter())
583− .filter(|text| !text.is_empty() && !text.chars().all(|c| "-=_ ".contains(c)))
584− .cloned()
585− .collect()
586−}
587−
588−#[cfg(test)]
589−mod tests {
590− use super::*;
591−
592− const BASE: &str = "user = {}\nimmutable = []\ntree = 0\nlabelled = 0\n";
593−
594− #[test]
595− fn lists_custom_names_types_and_docs_without_evaluation() {
596− let src = format!("{BASE}-- A custom greeting.\nhello = crash \"not evaluated\"\nAlias = Text -- a useful type\n");
597− let list = command_list(&src, "CUSTOM GREETING").unwrap();
598− assert!(list.contains("hello (definition; no signature)"));
599− assert!(!list.contains("immutable"));
600− let help = command_help(&src, "Alias").unwrap().unwrap();
601− assert!(help.contains("Alias = Text"));
602− assert!(help.contains("a useful type"));
603− assert!(help.contains("Source: line 7"));
604− }
605−
606− #[test]
607− fn signatures_keep_function_argument_parentheses() {
608− let src = format!("{BASE}(.) : (b -> c) -> (a -> b) -> a -> c -- compose\n");
609− let help = command_help(&src, "(.)").unwrap().unwrap();
610− assert!(help.contains("(.) : (b -> c) -> (a -> b) -> a -> c"));
611− assert!(help.contains("Builtin declared in config"));
612− assert!(help.contains("compose"));
613− }
614−
615− #[test]
616− fn comments_handle_strings_blocks_and_multiline_types() {
617− let src = format!("{BASE}message = \"-- not a doc\"\n\n{{- Useful\n block docs. -}}\nShape = {{ value : Text\n , count : Int }} -- shape details\n -- continued details\n\n-- Different docs.\nother = 1\n");
618− let message = command_help(&src, "message").unwrap().unwrap();
619− assert!(!message.contains("not a doc"));
620− let shape = command_help(&src, "Shape").unwrap().unwrap();
621− assert!(shape.contains("Useful\nblock docs."));
622− assert!(shape.contains("shape details\ncontinued details"));
623− assert!(!shape.contains("Different docs"));
624− assert!(shape.contains("Source: line 9"));
625− }
626−
627− #[test]
628− fn inline_continuations_belong_to_the_preceding_declaration() {
629− let src = format!("{BASE}first = 1 -- first comment\n -- first continuation\nsecond = 2\n\n -- third comment\nthird = 3\n");
630− let first = command_help(&src, "first").unwrap().unwrap();
631− assert!(first.contains("first comment\nfirst continuation"));
632− let second = command_help(&src, "second").unwrap().unwrap();
633− assert!(!second.contains("first continuation"));
634− assert!(!second.contains("third comment"));
635− assert!(command_help(&src, "third")
636− .unwrap()
637− .unwrap()
638− .contains("third comment"));
639− }
640−
641− #[test]
642− fn validates_config_and_does_not_resolve_ids() {
643− assert!(command_list("broken =", "").is_err());
644− assert!(command_help("x = 1", "x")
645− .unwrap_err()
646− .contains("must define"));
647− let src = format!("{BASE}revision = @zzzz\n");
648− assert!(command_help(&src, "revision").unwrap().is_some());
649− assert!(command_help(&src, "missing").unwrap().is_none());
650− assert!(command_list(&src, "no-such-query")
651− .unwrap()
652− .contains("No matching"));
653− }
654−
655− #[test]
656− fn default_config_documents_real_names() {
657− let src = include_str!("../config.j");
658− let help = command_help(src, "squash").unwrap().unwrap();
659− assert!(help.contains("squash : Edit"));
660− assert!(help.contains("Fold the focus entirely into its parent"));
661− let list = command_list(src, "edit").unwrap();
662− assert!(list.contains("squash : Edit"));
663− assert!(list.contains("describe : Text -> Edit"));
664− assert!(reserved_help("push")
665− .unwrap()
666− .contains("j 'push (label \"feature\" here)'"));
667− }
668−}
src/jj.rs+443−3137
@@ -6,38 +6,33 @@
6use crate::config::Config;
6use crate::config::Config;
7use crate::domain::{Backend, MetaInfo, ROOT_ID};
7use crate::domain::{Backend, MetaInfo, ROOT_ID};
8use crate::eval::Interp;
8use crate::eval::Interp;
9−use crate::value::{BlobContent, BlobKind, BlobVal, ConflictSide, Crash, LazyBlob, ListVal, ThunkVal, Value};
9+use crate::value::{BlobContent, BlobKind, BlobVal, Crash, LazyBlob, Value};
10−use futures::{StreamExt, TryStreamExt};
11use jj_lib::backend::{CopyId, Signature, Timestamp, TreeValue};
10use jj_lib::backend::{CopyId, Signature, Timestamp, TreeValue};
12use jj_lib::commit::Commit;
11use jj_lib::commit::Commit;
13use jj_lib::config::{ConfigLayer, ConfigSource, StackedConfig};
12use jj_lib::config::{ConfigLayer, ConfigSource, StackedConfig};
14−use jj_lib::conflicts::ConflictMarkerStyle;
15use jj_lib::default_backend_factories::{default_backend_factories, default_working_copy_factories};
13use jj_lib::default_backend_factories::{default_backend_factories, default_working_copy_factories};
16use jj_lib::gitignore::GitIgnoreFile;
14use jj_lib::gitignore::GitIgnoreFile;
17−use jj_lib::local_working_copy::{FileState, FileStates, FileType, LocalWorkingCopy, TreeState, TreeStateSettings};
18−use jj_lib::matchers::{DifferenceMatcher, EverythingMatcher, FilesMatcher, NothingMatcher};
15+use jj_lib::matchers::{EverythingMatcher, NothingMatcher};
19use jj_lib::merge::{Diff, Merge};
16use jj_lib::merge::{Diff, Merge};
20use jj_lib::merged_tree::MergedTree;
17use jj_lib::merged_tree::MergedTree;
21use jj_lib::merged_tree_builder::MergedTreeBuilder;
18use jj_lib::merged_tree_builder::MergedTreeBuilder;
22−use jj_lib::backend::{CommitId, FileId, MergedTreeValueExt, TreeId};
19+use jj_lib::backend::{CommitId, FileId};
23use jj_lib::object_id::ObjectId;
20use jj_lib::object_id::ObjectId;
24use jj_lib::op_store::OperationId;
21use jj_lib::op_store::OperationId;
25use jj_lib::ref_name::{RefName, RemoteName, WorkspaceName, WorkspaceNameBuf};
22use jj_lib::ref_name::{RefName, RemoteName, WorkspaceName, WorkspaceNameBuf};
26use jj_lib::repo::{ReadonlyRepo, Repo};
23use jj_lib::repo::{ReadonlyRepo, Repo};
27−use jj_lib::repo_path::{RepoPath, RepoPathBuf, RepoPathComponent, RepoPathComponentBuf};
24+use jj_lib::repo_path::{RepoPath, RepoPathBuf, RepoPathComponentBuf};
28−use jj_lib::revset::{ResolvedRevsetExpression, RevsetStreamExt};
29use jj_lib::settings::UserSettings;
25use jj_lib::settings::UserSettings;
30use jj_lib::store::Store;
26use jj_lib::store::Store;
31−use jj_lib::transaction::UnpublishedOperation;
32−use jj_lib::working_copy::{LockedWorkingCopy, SnapshotError, SnapshotOptions};
27+use jj_lib::working_copy::SnapshotOptions;
33use jj_lib::workspace::Workspace;
28use jj_lib::workspace::Workspace;
34−use std::cell::{Cell, RefCell};
29+use std::cell::RefCell;
35use std::collections::{BTreeMap, BTreeSet, HashMap};
30use std::collections::{BTreeMap, BTreeSet, HashMap};
36use std::future::Future;
31use std::future::Future;
37use std::os::unix::io::AsRawFd;
32use std::os::unix::io::AsRawFd;
38use std::path::PathBuf;
33use std::path::PathBuf;
39−use std::rc::{Rc, Weak};
34+use std::rc::Rc;
40−use std::sync::{Arc, Mutex, OnceLock};
35+use std::sync::{Arc, Mutex};
41
36
42fn block_on<F: Future>(f: F) -> F::Output {
37fn block_on<F: Future>(f: F) -> F::Output {
43 pollster::block_on(f)
38 pollster::block_on(f)
@@ -45,15 +40,6 @@ fn block_on<F: Future>(f: F) -> F::Output {
45
40
46type OpenError = (u8, String);
41type OpenError = (u8, String);
47
42
48−/// The successful effects of a push, after duplicate requests are merged.
49−/// All zeroes means no bookmarks were selected and nothing was sent or saved.
50−#[derive(Clone, Debug, Default, PartialEq, Eq)]
51−pub struct PushOutcome {
52− pub published: usize,
53− pub deleted: usize,
54− pub unchanged: usize,
55−}
56−
57// ----------------------------------------------------------------------
43// ----------------------------------------------------------------------
58// locating and opening the repository (§7.1)
44// locating and opening the repository (§7.1)
59// ----------------------------------------------------------------------
45// ----------------------------------------------------------------------
@@ -78,7 +64,6 @@ fn toml_string(s: &str) -> String {
78 '\n' => out.push_str("\\n"),
64 '\n' => out.push_str("\\n"),
79 '\r' => out.push_str("\\r"),
65 '\r' => out.push_str("\\r"),
80 '\t' => out.push_str("\\t"),
66 '\t' => out.push_str("\\t"),
81− c if c.is_ascii_control() => out.push_str(&format!("\\u{:04X}", c as u32)),
82 c => out.push(c),
67 c => out.push(c),
83 }
68 }
84 }
69 }
@@ -267,1166 +252,31 @@ pub struct JjInner {
267 repo: Mutex<Arc<ReadonlyRepo>>,
252 repo: Mutex<Arc<ReadonlyRepo>>,
268 workspace_name: WorkspaceNameBuf,
253 workspace_name: WorkspaceNameBuf,
269 /// visible map backing the `Backend` lookups during evaluation; filled by
254 /// visible map backing the `Backend` lookups during evaluation; filled by
270− /// build_interp with the post-snapshot map, or the loaded one when the
255+ /// build_interp with the post-snapshot map
271− /// snapshot goes into a new child of an immutable focus (§7.2)
272 visible: Mutex<Option<Arc<VisibleRepo>>>,
256 visible: Mutex<Option<Arc<VisibleRepo>>>,
273 lock_guard: Mutex<Option<FileLock>>,
257 lock_guard: Mutex<Option<FileLock>>,
274− /// the snapshot's unpublished rewrite, kept between build_interp and
258+ /// the snapshot transaction and working-copy lock, held between
275− /// persist so the snapshot and the expression's edits form one jj
259+ /// build_interp and persist so the snapshot and the expression's edits
276− /// operation (§1.2); dropped uncommitted by printing runs. The
260+ /// form one jj operation (§1.2); dropped uncommitted by printing runs
277− /// working-copy lock is not held: its saved state is still the tree
278− /// recorded before the snapshot until `persist`'s checkout scans the
279− /// directory again
280 pending: Mutex<Option<PendingSnapshot>>,
261 pending: Mutex<Option<PendingSnapshot>>,
281− /// whether the workspace's filesystem folds case, probed on first use
282− /// (`Backend::folds_case`)
283− folds_case: OnceLock<bool>,
284− /// whether the workspace's filesystem holds each name longer than
285− /// `NAME_MAX` asked about so far (`Backend::name_fits`)
286− long_names: Mutex<HashMap<String, bool>>,
287}
262}
288
263
289/// holding the file keeps the flock
264/// holding the file keeps the flock
290pub struct FileLock(#[allow(dead_code)] std::fs::File);
265pub struct FileLock(#[allow(dead_code)] std::fs::File);
291
266
267+#[allow(dead_code)]
292struct PendingSnapshot {
268struct PendingSnapshot {
293 /// repo as loaded at head, before the snapshot
269 /// repo as loaded at head, before the snapshot
294 pre_repo: Arc<ReadonlyRepo>,
270 pre_repo: Arc<ReadonlyRepo>,
295− /// the snapshot's tree for the wc commit, to be folded into the
271+ /// the snapshot's rewritten wc commit and tree, to be folded into the
296 /// persisting operation (§1.2 step 8, §7.7)
272 /// persisting operation (§1.2 step 8, §7.7)
273+ new_wc_id: CommitId,
297 tree: MergedTree,
274 tree: MergedTree,
298− /// false when the wc commit is immutable: the snapshot then belongs to
299− /// the focus's new child (§7.2), and the wc commit is kept as stored
300− fold_into_wc: bool,
301− /// the tree the scan found in the working directory, which `persist`'s
302− /// checkout starts from: `tree` itself, unless the working copy was
303− /// stale and `tree` merges the directory's changes onto the commit
304− /// (`snapshot_tree`)
305− scanned: MergedTree,
306−}
307−
308−/// How every scan of the working directory reads it (§7.4): each file
309−/// not ignored by the directory's own `.gitignore`s is tracked, whatever
310−/// its size. `persist`'s checkout rescans with the same options and
311−/// relies on getting the snapshot's tree back.
312−fn snapshot_options() -> SnapshotOptions<'static> {
313− SnapshotOptions {
314− base_ignores: GitIgnoreFile::empty(),
315− progress: None,
316− start_tracking_matcher: &EverythingMatcher,
317− force_tracking_matcher: &NothingMatcher,
318− max_new_file_size: u64::MAX,
319− }
320−}
321−
322−/// Scan the working directory with `options`, as `wc.snapshot` does, where
323−/// `base` is the tree the working copy's state records, as `saved` holds
324−/// it; return the tree scanned. jj's scan loses a regular file found where
325−/// `base` has a conflict it cannot write a file into, one with a side that
326−/// is no file, and keeps the conflict, so the file reaches no tree (§7.4):
327−///
328−/// - at a conflicted path, where a checkout wrote jj's description of the
329−/// sides and a file written since holds something else
330−/// (`FilesOnConflicts::at`). The state is first reset to a resolved file
331−/// there, so that the scan reads the file whole, as it reads any tracked
332−/// file; the path stays tracked where a `.gitignore` has come to match it.
333−/// - above one, where `base` has a directory holding a conflict, whose
334−/// differing sides the scan keeps at the path while it removes the
335−/// entries below (and a debug build fails an assertion). Such a file is
336−/// first left untracked, which scans it as if it had been moved away and
337−/// records the directory's removal; a second scan then records the file
338−/// where nothing is, as a later run would once it was put back.
339−async fn scan(
340− wc: &mut dyn LockedWorkingCopy,
341− root: &std::path::Path,
342− base: &MergedTree,
343− saved: &SavedState,
344− options: &SnapshotOptions<'_>,
345−) -> Result<MergedTree, String> {
346− let files = files_on_conflicts(root, base, Some(saved))?;
347− let mut state = base.clone();
348− if !files.at.is_empty() {
349− // the empty file: a reset records the file at each path it changes
350− // as an empty one, not executable, modified at the epoch, so the
351− // scan reads any other file there, and keeps this one for such a file
352− let store = base.store();
353− let id = store.write_file(RepoPath::root(), &mut &[][..]).await.map_err(|e| e.to_string())?;
354− let copy_id = CopyId::placeholder();
355− let empty = Merge::normal(TreeValue::File { id, executable: false, copy_id });
356− state = reset_paths(wc, &state, files.at.into_iter().map(|path| (path, empty.clone()))).await?;
357− }
358− if !files.over.is_empty() {
359− let untracked = FilesMatcher::new(&files.over);
360− let start = DifferenceMatcher::new(options.start_tracking_matcher, &untracked);
361− let first = SnapshotOptions { start_tracking_matcher: &start, ..options.clone() };
362− state = scan_pass(wc, root, &state, &first).await?;
363− // a state that agrees with its tree tracks every entry below the
364− // directory, so none is left; were a conflict left below, the
365− // second scan would lose the file after all
366− if let Some(path) = files_on_conflicts(root, &state, None)?.over.first() {
367− return Err(format!(
368− "cannot record the file `{}` in place of a directory holding a conflict",
369− path.as_internal_file_string()
370− ));
371− }
372− }
373− scan_pass(wc, root, &state, options).await
374−}
375−
376−/// `wc.snapshot(options)`, where `state` is the tree the working copy's
377−/// state records; return the tree scanned. jj's scan reads a directory a
378−/// `.gitignore` ignores only at the paths the state tracks there, and where
379−/// it cannot look at one of them because a directory above it has been
380−/// replaced by a file, it fails instead of taking the path for deleted, as
381−/// it takes one it does not find. The paths below a file are then dropped
382−/// from the state, which records them as deleted, and the directory is
383−/// scanned again (§7.4). A scan fails so while it walks the directory,
384−/// before it changes anything in the state.
385−async fn scan_pass(
386− wc: &mut dyn LockedWorkingCopy,
387− root: &std::path::Path,
388− state: &MergedTree,
389− options: &SnapshotOptions<'_>,
390−) -> Result<MergedTree, String> {
391− let err = match wc.snapshot(options).await {
392− Ok((scanned, _stats)) => return Ok(scanned),
393− Err(err) => err,
394− };
395− let not_a_dir = |e: &std::io::Error| e.kind() == std::io::ErrorKind::NotADirectory;
396− let SnapshotError::Other { err: source, .. } = &err else {
397− return Err(err.to_string());
398− };
399− if !source.downcast_ref::<std::io::Error>().is_some_and(not_a_dir) {
400− return Err(err.to_string());
401− }
402− // one `lstat` per path the state tracks, only once a scan has failed
403− let mut gone = Vec::new();
404− for (path, value) in state.entries() {
405− value.map_err(|e| e.to_string())?;
406− let Ok(disk) = path.to_fs_path(root) else {
407− continue;
408− };
409− if disk.symlink_metadata().is_err_and(|e| not_a_dir(&e)) {
410− gone.push((path, Merge::absent()));
411− }
412− }
413− if gone.is_empty() {
414− return Err(err.to_string());
415− }
416− reset_paths(wc, state, gone).await?;
417− let (scanned, _stats) = wc.snapshot(options).await.map_err(|e| e.to_string())?;
418− Ok(scanned)
419−}
420−
421−/// Give the working copy's state, which records `state`, the tree `state`
422−/// with `values` at their paths, and return it; nothing in the working
423−/// directory is read or written. A reset takes each path it changes for one
424−/// the next scan is to read again.
425−async fn reset_paths(
426− wc: &mut dyn LockedWorkingCopy,
427− state: &MergedTree,
428− values: impl IntoIterator<Item = (RepoPathBuf, jj_lib::backend::MergedTreeValue)>,
429−) -> Result<MergedTree, String> {
430− let mut builder = MergedTreeBuilder::new(state.clone());
431− for (path, value) in values {
432− builder.set_or_remove(path, value);
433− }
434− let tree = builder.write_tree().await.map_err(|e| e.to_string())?;
435− // a reset reads only the commit's tree
436− let commit = with_tree(&state.store().root_commit(), tree.clone());
437− wc.reset(&commit).await.map_err(|e| e.to_string())?;
438− Ok(tree)
439−}
440−
441−/// Where the working directory holds a regular file that jj's scan reads
442−/// into no tree, as `tree` has a conflict there it cannot write a file into
443−struct FilesOnConflicts {
444− /// the conflicted paths with a side that is no file (a directory, a
445− /// symlink, a submodule), which a checkout writes as jj's description of
446− /// the sides, whose file was written since and describes no such
447− /// conflict
448− at: Vec<RepoPathBuf>,
449− /// the directories holding a conflict, that is those above a conflicted
450− /// path: a directory's sides differ exactly when a conflict is below
451− /// it, as jj writes no tree whose sides differ where every path resolves
452− over: Vec<RepoPathBuf>,
453−}
454−
455−/// Where the working copy saves its state, to read back what the state a
456−/// scan's lock loaded records of a file (`files_on_conflicts`). Nothing is
457−/// read until a scan asks.
458−struct SavedState {
459− store: Arc<Store>,
460− root: PathBuf,
461− path: PathBuf,
462− settings: UserSettings,
463−}
464−
465−impl SavedState {
466− /// Where `ws` saves its working copy's state; taken before the lock,
467− /// which borrows `ws`
468− fn of(ws: &Workspace) -> SavedState {
469− let root = ws.workspace_root().to_owned();
470− let path = match ws.working_copy().downcast_ref::<LocalWorkingCopy>() {
471− Some(wc) => wc.state_path().to_owned(),
472− None => root.join(".jj").join("working_copy"),
473− };
474− SavedState { store: ws.repo_loader().store().clone(), root, path, settings: ws.settings().clone() }
475− }
476−
477− /// The state as saved, where it records `tree`, with the time it was
478− /// saved; None where it cannot be read or records another tree. A state
479− /// file that is not there is not read, as jj would write one to read.
480− fn load(&self, tree: &MergedTree) -> Option<(TreeState, i64)> {
481− let meta = self.path.join("tree_state").symlink_metadata().ok()?;
482− let saved_at = millis(meta.modified().ok()?)?;
483− let settings = TreeStateSettings::try_from_user_settings(&self.settings).ok()?;
484− let state = TreeState::load(self.store.clone(), self.root.clone(), self.path.clone(), &settings).ok()?;
485− (state.current_tree().tree_ids() == tree.tree_ids()).then_some((state, saved_at))
486− }
487−}
488−
489−/// A modification time in milliseconds since the epoch, as jj's working
490−/// copy records one
491−fn millis(time: std::time::SystemTime) -> Option<i64> {
492− match time.duration_since(std::time::UNIX_EPOCH) {
493− Ok(d) => i64::try_from(d.as_millis()).ok(),
494− Err(e) => i64::try_from(e.duration().as_millis()).ok().map(|m| -m),
495− }
496−}
497−
498−/// Whether the working copy's state, saved at `saved_at`, records `state`
499−/// for the regular file `meta` describes as it is now: jj's own test of a
500−/// file left as it was (`FileState::is_clean`, the executable bit aside),
501−/// under which it reads nothing. A file modified in the millisecond the
502−/// state was saved, or later, may have been written after it was.
503−fn left_as_recorded(state: &FileState, meta: &std::fs::Metadata, saved_at: i64) -> bool {
504− matches!(state.file_type, FileType::Normal { .. })
505− && state.size == meta.len()
506− && meta.modified().ok().and_then(millis) == Some(state.mtime.0)
507− && state.mtime.0 < saved_at
508−}
509−
510−/// The terms of a conflict that `text`, jj's description of one, names:
511−/// those removed and those added, each without the label a checkout may
512−/// write after it, with each removed that is also added cancelled against
513−/// it, as simplifying the conflict does, sorted; None for text that is no
514−/// description. Two descriptions naming the same terms describe one
515−/// conflict, whatever its labels or the sides it has lost since.
516−fn described_terms(text: &[u8]) -> Option<(Vec<&str>, Vec<&str>)> {
517− let lines = std::str::from_utf8(text).ok()?.strip_prefix("Conflict:\n")?;
518− if !lines.is_empty() && !lines.ends_with('\n') {
519− return None;
520− }
521− let (mut removes, mut adds) = (Vec::new(), Vec::new());
522− for line in lines.split_terminator('\n') {
523− let (terms, term) = match (line.strip_prefix(" Removing "), line.strip_prefix(" Adding ")) {
524− (Some(term), _) => (&mut removes, term),
525− (_, Some(term)) => (&mut adds, term),
526− _ => return None,
527− };
528− // a term ends with its object's id, which holds no space
529− let id = term.find(" with id ")? + " with id ".len();
530− let end = term[id..].find(' ').map_or(term.len(), |n| id + n);
531− let (term, label) = term.split_at(end);
532− if !label.is_empty() && !(label.starts_with(" (") && label.ends_with(')')) {
533− return None;
534− }
535− terms.push(term);
536− }
537− removes.retain(|term| match adds.iter().position(|t| t == term) {
538− Some(n) => {
539− adds.swap_remove(n);
540− false
541− }
542− None => true,
543− });
544− removes.sort_unstable();
545− adds.sort_unstable();
546− Some((removes, adds))
547−}
548−
549−/// Look on disk at `tree`'s conflicts for `FilesOnConflicts`; with no
550−/// `saved`, for `over` only. Nothing is looked at when `tree` is resolved;
551−/// each directory above a conflict at most once, with one `lstat`; and a
552−/// conflict with a side that is no file below directories, with one more,
553−/// and a read where its file is as long as its description. Only a file
554−/// that holds something else is looked up in the saved state, which is read
555−/// once, and read itself where the state records it otherwise.
556−fn files_on_conflicts(
557− root: &std::path::Path,
558− tree: &MergedTree,
559− saved: Option<&SavedState>,
560−) -> Result<FilesOnConflicts, String> {
561− let mut files = FilesOnConflicts { at: Vec::new(), over: Vec::new() };
562− // whether each directory looked at so far is one on disk
563− let mut dirs: HashMap<RepoPathBuf, bool> = HashMap::new();
564− // the state as saved, read once a file differs from its description,
565− // and what it records of the files, taken once
566− let loaded: std::cell::OnceCell<Option<(TreeState, i64)>> = std::cell::OnceCell::new();
567− let recorded: std::cell::OnceCell<Option<(FileStates<'_>, i64)>> = std::cell::OnceCell::new();
568− for (path, value) in tree.conflicts() {
569− let value = value.map_err(|e| e.to_string())?;
570− let parent = path.parent().expect("a conflicted path has a parent");
571− let above: Vec<&RepoPath> = parent.ancestors().take_while(|d| !d.is_root()).collect();
572− // from the top down, stopping at the first that is not a directory
573− let below_dirs = above.into_iter().rev().all(|dir| match dirs.get(dir) {
574− Some(&is_dir) => is_dir,
575− None => {
576− let meta = dir.to_fs_path(root).ok().and_then(|p| p.symlink_metadata().ok());
577− if meta.as_ref().is_some_and(|m| m.is_file()) {
578− files.over.push(dir.to_owned());
579− }
580− let is_dir = meta.is_some_and(|m| m.is_dir());
581− dirs.insert(dir.to_owned(), is_dir);
582− is_dir
583− }
584− });
585− let Some(saved) = saved.filter(|_| below_dirs && value.to_file_merge().is_none()) else {
586− continue;
587− };
588− let Some((disk, meta)) = path.to_fs_path(root).ok().and_then(|disk| {
589− let meta = disk.symlink_metadata().ok()?;
590− meta.is_file().then_some((disk, meta))
591− }) else {
592− continue;
593− };
594− // what jj's checkout writes for it (§7.4)
595− let written = value.describe(tree.labels());
596− let mut bytes = None;
597− if meta.len() == written.len() as u64 {
598− match std::fs::read(&disk) {
599− Ok(read) if read == written.as_bytes() => continue,
600− Ok(read) => bytes = Some(read),
601− Err(_) => continue,
602− }
603− }
604− // the file a checkout wrote, left as it was: it describes the
605− // conflict as the checkout had it, which the state may since
606− // record otherwise with no file written, as a scan that drops a
607− // pair of sides that cancel, or a reset that only relabels, does
608− let states = recorded.get_or_init(|| {
609− let (state, saved_at) = loaded.get_or_init(|| saved.load(tree)).as_ref()?;
610− Some((state.file_states(), *saved_at))
611− });
612− if states.as_ref().is_some_and(|(file_states, saved_at)| {
613− file_states.get(&path).is_some_and(|state| left_as_recorded(&state, &meta, *saved_at))
614− }) {
615− continue;
616− }
617− // written since, or not known to be left: the conflict is kept
618− // while the file describes it, as when an editor saves it unchanged
619− let Some(bytes) = bytes.or_else(|| std::fs::read(&disk).ok()) else {
620− continue;
621− };
622− let unlabelled = value.describe(&jj_lib::conflict_labels::ConflictLabels::unlabeled());
623− let terms = described_terms(unlabelled.as_bytes());
624− if terms.is_none() || described_terms(&bytes) != terms {
625− files.at.push(path);
626− }
627− }
628− Ok(files)
629−}
630−
631−/// The tree a snapshot records in the working-copy commit, whose tree is
632−/// `wc`, where the working copy's state records `state` and a scan of the
633−/// working directory found `scanned` (§7.4): `scanned` itself where the
634−/// state is the commit's, and also where it is `current`, saved at the
635−/// operation the repository is at, which jj takes for the files last
636−/// checked out whatever it records: one lost, which jj makes again
637−/// recording no file, or one put in its place. Such a state may record none
638−/// of the commit's conflicts, so a file holding what a checkout wrote for
639−/// one is read back as it there (`conflicts_written`). Otherwise the state
640−/// is stale (a checkout that failed or was cut short after its operation
641−/// was recorded, a `fetch` that rebased the commit, jj run with
642−/// `--ignore-working-copy` or from another workspace), and what the
643−/// directory holds is a change to `state` rather than to the commit: that
644−/// change is replayed onto the commit, a three-way merge with `state` as
645−/// the base, in which a path both changed differently is a conflict, but
646−/// for one setting a directory against anything else, which takes what the
647−/// working directory holds there, as where the state is current. What a
648−/// checkout cut short wrote is the commit's own content, which the merge
649−/// takes as it is: where the directory holds what the commit does, the base
650−/// is taken to hold it too, so that a conflict the state records there,
651−/// whose sides would not cancel against the commit's, does not make one of
652−/// a change both made. That includes a conflict the checkout wrote, which
653−/// the scan read as its text (`conflicts_written`).
654−async fn snapshot_tree(
655− root: &std::path::Path,
656− markers: ConflictMarkerStyle,
657− wc: &MergedTree,
658− state: &MergedTree,
659− current: bool,
660− scanned: &MergedTree,
661−) -> Result<MergedTree, String> {
662− if state.tree_ids() == wc.tree_ids() {
663− return Ok(scanned.clone());
664− }
665− if current {
666− return conflicts_written(root, markers, wc, state, scanned).await;
667− }
668− if scanned.tree_ids() == state.tree_ids() {
669− return Ok(wc.clone());
670− }
671− let scanned = &conflicts_written(root, markers, wc, state, scanned).await?;
672− let err = |e: jj_lib::backend::BackendError| e.to_string();
673− // the paths where the directory holds something other than the commit
674− let mut apart = std::collections::HashSet::new();
675− let mut diff = wc.diff_stream(scanned, &EverythingMatcher);
676− while let Some(entry) = diff.next().await {
677− entry.values.map_err(err)?;
678− apart.insert(entry.path);
679− }
680− // the base: the state, but for what the directory changed as the
681− // commit did
682− let mut base = MergedTreeBuilder::new(state.clone());
683− let mut same = false;
684− let mut diff = state.diff_stream(scanned, &EverythingMatcher);
685− while let Some(entry) = diff.next().await {
686− let Diff { after, .. } = entry.values.map_err(err)?;
687− if !apart.contains(&entry.path) {
688− base.set_or_remove(entry.path, after);
689− same = true;
690− }
691− }
692− let base = if same { base.write_tree().await.map_err(err)? } else { state.clone() };
693− let merged = MergedTree::merge(Merge::from_removes_adds(
694− vec![(base.clone(), "last checkout".to_string())],
695− vec![
696− (wc.clone(), "working-copy commit".to_string()),
697− (scanned.clone(), "working directory".to_string()),
698− ],
699− ))
700− .await
701− .map_err(err)?;
702− // A conflict setting a directory against a file, a symlink or nothing,
703− // which jj keeps at the path itself, cannot be a blob that lists the
704− // directory's entries (§7.3), and a checkout would write jj's
705− // description of it in place of what the directory holds there,
706− // removing a directory's files: where the directory changed the path,
707− // take what it holds, as a snapshot over a current state does. The
708− // commit's change there is left in the operation before. Where the
709− // directory holds what the base does, the merge took the commit's value
710− // as it is, so such a conflict is one the commit already has, and it
711− // stays. A tree with no conflict is not walked.
712− let mut kept = MergedTreeBuilder::new(merged.clone());
713− let mut mixed = false;
714− for (path, value) in merged.conflicts() {
715− if value.map_err(err)?.iter().any(|term| matches!(term, Some(TreeValue::Tree(_)))) {
716− let held = scanned.path_value(&path).await.map_err(err)?;
717− if held == base.path_value(&path).await.map_err(err)? {
718− continue;
719− }
720− kept.set_or_remove(path, held);
721− mixed = true;
722− }
723− }
724− if !mixed {
725− return Ok(merged);
726− }
727− kept.write_tree().await.map_err(err)
728−}
729−
730−/// `scanned` with the commit's conflict at each path the working-copy state
731−/// records something else at, where the file holds what jj's checkout
732−/// writes for that conflict (`holds_written`): its markers, or its
733−/// description where a side is no file (§7.4). A checkout records the
734−/// conflict in the state as it writes the file, so a scan reads such a file
735−/// back as the conflict; one cut short, or one over a stale state, records
736−/// nothing, nor does a state made again where it was lost, and the scan
737−/// reads the file as the text it holds. Each of the commit's conflicts is
738−/// looked at, and a file read, only where neither the state nor the scan
739−/// has the conflict.
740−async fn conflicts_written(
741− root: &std::path::Path,
742− markers: ConflictMarkerStyle,
743− wc: &MergedTree,
744− state: &MergedTree,
745− scanned: &MergedTree,
746−) -> Result<MergedTree, String> {
747− let err = |e: jj_lib::backend::BackendError| e.to_string();
748− let mut tree = MergedTreeBuilder::new(scanned.clone());
749− let mut found = false;
750− for (path, value) in wc.conflicts() {
751− let value = value.map_err(err)?;
752− if state.path_value(&path).await.map_err(err)? == value
753− || scanned.path_value(&path).await.map_err(err)? == value
754− {
755− continue;
756− }
757− let Ok(disk) = path.to_fs_path(root) else {
758− continue;
759− };
760− if holds_written(wc.store(), &path, value.clone(), wc.labels(), markers, &disk).await? {
761− tree.set_or_remove(path, value);
762− found = true;
763− }
764− }
765− if !found {
766− return Ok(scanned.clone());
767− }
768− tree.write_tree().await.map_err(err)
769−}
770−
771−/// Whether `disk`, the entry at `path`, holds what a checkout writes there
772−/// for `value`: a file's content or a symlink's target, whatever its
773−/// executable bit, or, for a conflict, exactly its markers, compared byte
774−/// for byte (j's settings convert no line endings), or, where a side is no
775−/// file, a description of the same sides, whatever their labels and less
776−/// any it both removes and adds, as a scan reads one back
777−/// (`files_on_conflicts`); nothing is written to the store
778−async fn holds_written(
779− store: &Arc<Store>,
780− path: &RepoPath,
781− value: jj_lib::backend::MergedTreeValue,
782− labels: &jj_lib::conflict_labels::ConflictLabels,
783− markers: ConflictMarkerStyle,
784− disk: &std::path::Path,
785−) -> Result<bool, String> {
786− use jj_lib::conflicts::{self, MaterializedTreeValue};
787− let symlink = matches!(value.as_normal(), Some(TreeValue::Symlink(_)));
788− let Some(meta) = disk.symlink_metadata().ok().filter(|m| if symlink { m.is_symlink() } else { m.is_file() })
789− else {
790− return Ok(false);
791− };
792− let written = conflicts::materialize_tree_value(store, path, value, labels)
793− .await
794− .map_err(|e| e.to_string())?;
795− match written {
796− MaterializedTreeValue::Symlink { target, .. } => {
797− Ok(std::fs::read_link(disk).is_ok_and(|t| t.as_os_str() == target.as_str()))
798− }
799− MaterializedTreeValue::File(mut file) => {
800− let bytes = file.read_all(path).await.map_err(|e| e.to_string())?;
801− Ok(meta.len() == bytes.len() as u64 && std::fs::read(disk).is_ok_and(|b| b == bytes))
802− }
803− MaterializedTreeValue::FileConflict(file) => {
804− let options = conflicts::ConflictMaterializeOptions {
805− marker_style: markers,
806− marker_len: Some(conflicts::choose_materialized_conflict_marker_len(&file.contents)),
807− merge: store.merge_options().clone(),
808− };
809− let bytes: Vec<u8> =
810− conflicts::materialize_merge_result_to_bytes(&file.contents, &file.labels, &options).into();
811− Ok(meta.len() == bytes.len() as u64 && std::fs::read(disk).is_ok_and(|b| b == bytes))
812− }
813− MaterializedTreeValue::OtherConflict { id, .. } => {
814− let unlabelled = id.describe(&jj_lib::conflict_labels::ConflictLabels::unlabeled());
815− let terms = described_terms(unlabelled.as_bytes());
816− Ok(terms.is_some() && std::fs::read(disk).is_ok_and(|b| described_terms(&b) == terms))
817− }
818− _ => Ok(false),
819− }
820−}
821−
822−/// How the working copy writes a conflict, as `settings` say
823−fn marker_style(settings: &UserSettings) -> Result<ConflictMarkerStyle, String> {
824− Ok(TreeStateSettings::try_from_user_settings(settings).map_err(|e| e.to_string())?.conflict_marker_style)
825−}
826−
827−/// The first of the directories between the root and `path`, from the top
828−/// down, that is no directory on disk, with the type of what stands there
829−/// (none where nothing does, or it cannot be looked at), or none if each
830−/// is: a symlink is not one, and is not followed, as no directory below
831−/// one is looked at. Each directory not looked at before costs one
832−/// `lstat`, and what it found is kept in `dirs`.
833−fn first_not_a_dir(
834− root: &std::path::Path,
835− path: &RepoPath,
836− dirs: &mut HashMap<RepoPathBuf, Option<std::fs::FileType>>,
837−) -> Option<(RepoPathBuf, Option<std::fs::FileType>)> {
838− let parent = path.parent()?;
839− let above: Vec<&RepoPath> = parent.ancestors().take_while(|d| !d.is_root()).collect();
840− for dir in above.into_iter().rev() {
841− let stands = match dirs.get(dir) {
842− Some(&stands) => stands,
843− None => {
844− let meta = dir.to_fs_path(root).ok().and_then(|p| p.symlink_metadata().ok());
845− let stands = meta.map(|m| m.file_type());
846− dirs.insert(dir.to_owned(), stands);
847− stands
848− }
849− };
850− if !stands.is_some_and(|t| t.is_dir()) {
851− return Some((dir.to_owned(), stands));
852− }
853− }
854− None
855−}
856−
857−/// The first path where the working directory, scanned again as
858−/// `rescanned`, no longer holds what it held as `on_disk`, the run's
859−/// snapshot of it, and a checkout from `on_disk` to `to` writes or removes
860−/// a file: a change made while the program ran, which no snapshot holds and
861−/// jj's checkout would write over without looking (§7.5 step 7). Paths are
862−/// compared file by file, as a directory is by what it holds; one lookup
863−/// in `to` per path the directory changed at.
864−async fn written_over(
865− on_disk: &MergedTree,
866− rescanned: &MergedTree,
867− to: &MergedTree,
868−) -> Result<Option<RepoPathBuf>, String> {
869− let err = |e: jj_lib::backend::BackendError| e.to_string();
870− let mut diff = on_disk.diff_stream(rescanned, &EverythingMatcher);
871− while let Some(entry) = diff.next().await {
872− let Diff { before, .. } = entry.values.map_err(err)?;
873− // a diff has no tree on either side
874− let focus = to.path_value(&entry.path).await.map_err(err)?;
875− let focus = if focus.is_tree() { Merge::absent() } else { focus };
876− if focus != before {
877− return Ok(Some(entry.path));
878− }
879− }
880− Ok(None)
881−}
882−
883−/// What stands in the way of a path a checkout adds: `holder`, in the
884−/// working directory, which does not track it, at `path`, above it, or in
885−/// a directory at it
886−struct InTheWay {
887− path: RepoPathBuf,
888− holder: RepoPathBuf,
889−}
890−
891−impl InTheWay {
892− /// Whether it is a directory at `path`, holding `holder`
893− fn is_dir(&self) -> bool {
894− self.holder != self.path && self.holder.starts_with(&self.path)
895− }
896−
897− /// What a refused checkout says of it among others: a directory by its
898− /// own path, with what it holds, anything else by its path
899− fn named(&self) -> String {
900− if self.is_dir() {
901− format!(
902− "`{}` (a directory holding untracked files, such as `{}`)",
903− self.path.as_internal_file_string(),
904− self.holder.as_internal_file_string()
905− )
906− } else {
907− format!("`{}`", self.holder.as_internal_file_string())
908− }
909− }
910−}
911−
912−/// Why a checkout is refused over `in_the_way`, which is not empty, as
913−/// git names each untracked file in the way: one with the path the focus
914−/// has there, more by listing the first ten and counting the rest
915−fn in_the_way_of(in_the_way: &[InTheWay]) -> String {
916− const LISTED: usize = 10;
917− match in_the_way {
918− [one] if one.is_dir() => format!(
919− "it has `{0}`, and `{0}` in the working directory is a directory holding untracked files, such as `{1}`; move it aside",
920− one.path.as_internal_file_string(),
921− one.holder.as_internal_file_string()
922− ),
923− [one] => format!(
924− "it has `{}`, and an untracked `{}` in the working directory is in its way; move it aside",
925− one.path.as_internal_file_string(),
926− one.holder.as_internal_file_string()
927− ),
928− _ => {
929− let mut named: Vec<String> = in_the_way.iter().take(LISTED).map(InTheWay::named).collect();
930− let last = match in_the_way.len() - named.len() {
931− 0 => named.pop().unwrap_or_default(),
932− more => format!("{} more", more),
933− };
934− format!(
935− "untracked files in the working directory are in the way of files it has: {} and {}; move them aside",
936− named.join(", "),
937− last
938− )
939− }
940− }
941−}
942−
943−/// What stands in the working directory where a checkout from `from`, the
944−/// tree the directory holds as scanned, to `to` adds a file (or a symlink,
945−/// or a conflict): at each path where `to` has one and `from` has none.
946−/// jj's checkout writes nothing over anything already there, and skips
947−/// the path without failing, recording it as written, so the next scan
948−/// would read what stands there into the focus, an ignored file's content
949−/// in place of the focus's, or take the focus's file for deleted (§7.4).
950−/// What `from` tracks there the checkout removes first; trees of empty
951−/// directories, which no scan sees, and a file holding just what the
952−/// checkout writes there (`holds_written`), as a checkout cut short leaves
953−/// at an ignored path, are cleared for it (`clear_the_way`). The result is
954−/// the paths where nothing else stands, or, walking the whole diff all the
955−/// same, each thing standing where anything else does (`InTheWay`), in
956−/// path order, what stands above several paths once. No symlink is
957−/// followed. Each path costs one `lstat`, and each directory above one not
958−/// looked at before one more; a file standing at a path is read, and a
959−/// directory read until something untracked is found in it, each entry in
960−/// it that is no directory looked up in `from`.
961−async fn look_where_added(
962− root: &std::path::Path,
963− markers: ConflictMarkerStyle,
964− from: &MergedTree,
965− to: &MergedTree,
966−) -> Result<Result<Vec<RepoPathBuf>, Vec<InTheWay>>, String> {
967− let err = |e: jj_lib::backend::BackendError| e.to_string();
968− let mut dirs = HashMap::new();
969− // each thing that is no directory looked up in `from`, above a path
970− let mut looked_up = std::collections::HashSet::new();
971− let mut clear = Vec::new();
972− let mut in_the_way = Vec::new();
973− let mut diff = from.diff_stream(to, &EverythingMatcher);
974− while let Some(entry) = diff.next().await {
975− let Diff { before, after } = entry.values.map_err(err)?;
976− // a submodule's directory is the submodule's own, which jj keeps
977− if before.is_present() || after.is_absent() || matches!(after.as_normal(), Some(TreeValue::GitSubmodule(_))) {
978− continue;
979− }
980− let path = entry.path;
981− if let Some((dir, stands)) = first_not_a_dir(root, &path, &mut dirs) {
982− // where nothing stands above the path, or what `from` tracks
983− // and the checkout removes, nothing stands at it
984− if stands.is_some() && looked_up.insert(dir.clone()) {
985− let is_tracked = from.path_value(&dir).await.map_err(err)?.is_present();
986− if !is_tracked {
987− in_the_way.push(InTheWay { path, holder: dir });
988− }
989− }
990− continue;
991− }
992− let Ok(disk) = path.to_fs_path(root) else {
993− continue;
994− };
995− let Ok(meta) = disk.symlink_metadata() else {
996− continue;
997− };
998− let holder = if meta.is_dir() {
999− untracked_below(&disk, &path, from)
1000− } else if holds_written(to.store(), &path, after, to.labels(), markers, &disk).await? {
1001− None
1002− } else {
1003− Some(path.clone())
1004− };
1005− match holder {
1006− Some(holder) => in_the_way.push(InTheWay { path, holder }),
1007− None => clear.push(path),
1008− }
1009− }
1010− if in_the_way.is_empty() {
1011− Ok(Ok(clear))
1012− } else {
1013− Ok(Err(in_the_way))
1014− }
1015−}
1016−
1017−/// Something that is no directory in the directory `disk`, standing at
1018−/// `path`, or in a directory in it, that `from` does not track, or a
1019−/// directory that cannot be read; none where it holds nothing else. Entries
1020−/// are told apart without following a symlink, and `from` is read below
1021−/// `path` only once there is something to look up.
1022−fn untracked_below(disk: &std::path::Path, path: &RepoPath, from: &MergedTree) -> Option<RepoPathBuf> {
1023− let mut tracked: Option<std::collections::HashSet<RepoPathBuf>> = None;
1024− let mut dirs = vec![(disk.to_owned(), path.to_owned())];
1025− while let Some((dir, at)) = dirs.pop() {
1026− let Ok(entries) = std::fs::read_dir(&dir) else {
1027− return Some(at);
1028− };
1029− for entry in entries {
1030− let Ok(entry) = entry else {
1031− return Some(at);
1032− };
1033− let name = entry.file_name();
1034− // a name no path of a tree holds
1035− let Some(below) = name.to_str().and_then(|n| RepoPathComponent::new(n).ok()).map(|c| at.join(c)) else {
1036− return Some(at);
1037− };
1038− if entry.file_type().is_ok_and(|t| t.is_dir()) {
1039− dirs.push((entry.path(), below));
1040− continue;
1041− }
1042− let tracked = tracked.get_or_insert_with(|| {
1043− let under = jj_lib::matchers::PrefixMatcher::new([path]);
1044− from.entries_matching(&under).filter(|(_, value)| value.is_ok()).map(|(p, _)| p).collect()
1045− });
1046− if !tracked.contains(&below) {
1047− return Some(below);
1048− }
1049− }
1050− }
1051− None
1052−}
1053−
1054−/// Clear each of `paths`, where a checkout from the tree the working
1055−/// directory holds to `to` adds a file (`look_where_added`): a file there
1056−/// is moved aside while it holds what the checkout writes
1057−/// (`holds_written`), and in a directory there every directory holding no
1058−/// file is removed, deepest first, and it too once empty. A path below
1059−/// anything that is no directory on disk, a symlink included, is left
1060−/// alone, and no symlink in one is followed; a directory is removed only
1061−/// while empty, so a file put there meanwhile stays, as does one changed
1062−/// meanwhile, and the checkout skips the path (§7.5 step 7). So does one
1063−/// that cannot be moved, which the result names.
1064−async fn clear_the_way(
1065− root: &std::path::Path,
1066− markers: ConflictMarkerStyle,
1067− to: &MergedTree,
1068− paths: &[RepoPathBuf],
1069−) -> Aside {
1070− fn clear(dir: &std::path::Path) {
1071− if let Ok(entries) = std::fs::read_dir(dir) {
1072− for entry in entries.flatten() {
1073− if entry.file_type().is_ok_and(|t| t.is_dir()) {
1074− clear(&entry.path());
1075− }
1076− }
1077− }
1078− let _ = std::fs::remove_dir(dir);
1079− }
1080− let mut aside = Aside { root: root.to_owned(), dir: None, stuck: None, returned: false };
1081− let mut dirs = HashMap::new();
1082− for path in paths {
1083− if first_not_a_dir(root, path, &mut dirs).is_some() {
1084− continue;
1085− }
1086− let Ok(disk) = path.to_fs_path(root) else {
1087− continue;
1088− };
1089− let Ok(meta) = disk.symlink_metadata() else {
1090− continue;
1091− };
1092− if meta.is_dir() {
1093− clear(&disk);
1094− continue;
1095− }
1096− let Ok(value) = to.path_value(path).await else {
1097− continue;
1098− };
1099− if holds_written(to.store(), path, value, to.labels(), markers, &disk).await.unwrap_or(false) {
1100− if let Err(e) = aside.take(path, &disk) {
1101− aside.stuck.get_or_insert((path.clone(), e));
1102− }
1103− }
1104− }
1105− aside
1106−}
1107−
1108−/// The start of the name of each directory in `.jj` that `Aside` moves
1109−/// files into
1110−const ASIDE: &str = "aside";
1111−
1112−/// The files `clear_the_way` moved out of a checkout's way, each holding
1113−/// what the checkout writes where it stood, in a directory in `.jj` that
1114−/// holds each at its path in the working directory `root`, which no scan
1115−/// reads; the first file it could not move, with why; and whether the
1116−/// checkout returned rather than failing, and so wrote in full each file
1117−/// it wrote. Dropped, once the checkout has written what it could, it puts
1118−/// each back (`put_back`) where nothing stands now, as where a checkout
1119−/// that failed or skipped paths did not get to write, and, unless the
1120−/// checkout returned, where a start of it does, as where it was cut short
1121−/// writing it, and removes the directory with the rest: each holds just
1122−/// what the focus has at its path, which the repository keeps, and what
1123−/// stands there, what the checkout wrote or something put there since,
1124−/// stays (§7.5 step 7). One a run killed meanwhile left, the next puts
1125−/// back (`put_back_left`).
1126−struct Aside {
1127− root: std::path::PathBuf,
1128− dir: Option<tempfile::TempDir>,
1129− stuck: Option<(RepoPathBuf, std::io::Error)>,
1130− returned: bool,
1131−}
1132−
1133−impl Aside {
1134− /// Move `disk`, the file at `path`, into the directory, made on first
1135− /// use; renamed, it is neither copied nor followed if a symlink
1136− fn take(&mut self, path: &RepoPath, disk: &std::path::Path) -> std::io::Result<()> {
1137− let dir = match &mut self.dir {
1138− Some(dir) => dir,
1139− none => none.insert(tempfile::Builder::new().prefix(ASIDE).tempdir_in(self.root.join(".jj"))?),
1140− };
1141− let to = path.to_fs_path(dir.path()).map_err(std::io::Error::other)?;
1142− if let Some(parent) = to.parent() {
1143− std::fs::create_dir_all(parent)?;
1144− }
1145− std::fs::rename(disk, to)
1146− }
1147−}
1148−
1149−impl Drop for Aside {
1150− fn drop(&mut self) {
1151− if let Some(dir) = self.dir.take() {
1152− put_back(&self.root, dir.path(), !self.returned);
1153− }
1154− }
1155−}
1156−
1157−/// Put each file in `aside`, where `Aside` moved it, back at its path in
1158−/// the working directory `root` where nothing stands, making the
1159−/// directories above it that are missing, and, where the checkout may have
1160−/// been `cut` short, where a start of it stands, less than all of it, as a
1161−/// checkout cut short while writing it leaves (`cut_short`), which it
1162−/// replaces; one that cannot be put back, as something else stands there
1163−/// or in place of a directory above it, stays in `aside`. No symlink is
1164−/// followed.
1165−fn put_back(root: &std::path::Path, aside: &std::path::Path, cut: bool) {
1166− let missing = |p: &std::path::Path| {
1167− matches!(p.symlink_metadata(), Err(e) if e.kind() == std::io::ErrorKind::NotFound)
1168− };
1169− // whether each directory above `path` in `root` is one, or is made
1170− let dirs_above = |path: &std::path::Path| {
1171− let mut at = root.to_owned();
1172− for name in path.parent().into_iter().flat_map(|p| p.components()) {
1173− at.push(name);
1174− let made = match at.symlink_metadata() {
1175− Ok(meta) => meta.is_dir(),
1176− Err(e) => e.kind() == std::io::ErrorKind::NotFound && std::fs::create_dir(&at).is_ok(),
1177− };
1178− if !made {
1179− return false;
1180− }
1181− }
1182− true
1183− };
1184− let mut dirs = vec![std::path::PathBuf::new()];
1185− while let Some(dir) = dirs.pop() {
1186− let Ok(entries) = std::fs::read_dir(aside.join(&dir)) else {
1187− continue;
1188− };
1189− for entry in entries.flatten() {
1190− let path = dir.join(entry.file_name());
1191− if entry.file_type().is_ok_and(|t| t.is_dir()) {
1192− dirs.push(path);
1193− continue;
1194− }
1195− let disk = root.join(&path);
1196− // the directories above are looked at first, so that nothing
1197− // is looked at through a symlink
1198− if dirs_above(&path) && (missing(&disk) || (cut && cut_short(&disk, &entry.path()))) {
1199− let _ = std::fs::rename(entry.path(), disk);
1200− }
1201− }
1202− }
1203−}
1204−
1205−/// Whether `disk` is a file holding a start of `whole`, a file moved
1206−/// aside, and less than all of it, or, where `whole` is a symlink, an
1207−/// empty file: what jj's checkout leaves where it was cut short (a full
1208−/// disk, a run killed) while it wrote `whole` there, as it makes a file
1209−/// empty before it writes it, and tries a symlink's path with an empty
1210−/// file first. Reads no more of either than `disk` holds. No symlink is
1211−/// followed.
1212−fn cut_short(disk: &std::path::Path, whole: &std::path::Path) -> bool {
1213− use std::io::Read;
1214− use std::os::unix::fs::OpenOptionsExt;
1215− let (Ok(stands), Ok(copy)) = (disk.symlink_metadata(), whole.symlink_metadata()) else {
1216− return false;
1217− };
1218− if !stands.is_file() {
1219− return false;
1220− }
1221− if copy.is_symlink() {
1222− return stands.len() == 0;
1223− }
1224− if !copy.is_file() || stands.len() >= copy.len() {
1225− return false;
1226− }
1227− let stands_file = std::fs::OpenOptions::new().read(true).custom_flags(libc::O_NOFOLLOW).open(disk);
1228− let (Ok(mut stands_file), Ok(mut copy_file)) = (stands_file, std::fs::File::open(whole)) else {
1229− return false;
1230− };
1231− let (mut a, mut b) = (vec![0; 1 << 16], vec![0; 1 << 16]);
1232− let mut rest = stands.len();
1233− while rest > 0 {
1234− let n = rest.min(a.len() as u64) as usize;
1235− let same = stands_file.read_exact(&mut a[..n]).is_ok()
1236− && copy_file.read_exact(&mut b[..n]).is_ok()
1237− && a[..n] == b[..n];
1238− if !same {
1239− return false;
1240− }
1241− rest -= n as u64;
1242− }
1243− true
1244−}
1245−
1246−/// Put back what a run killed while its checkout ran left moved aside in
1247−/// `.jj` (`Aside`), over a write the kill may have cut short too, and
1248−/// remove the directory it left there. A run does this once it holds the
1249−/// repository's lock, before it looks at the working directory, so no
1250−/// other run is using such a directory.
1251−fn put_back_left(root: &std::path::Path) {
1252− let Ok(entries) = std::fs::read_dir(root.join(".jj")) else {
1253− return;
1254− };
1255− for entry in entries.flatten() {
1256− let named = entry.file_name().to_str().is_some_and(|n| n.starts_with(ASIDE));
1257− if named && entry.file_type().is_ok_and(|t| t.is_dir()) {
1258− put_back(root, &entry.path(), true);
1259− let _ = std::fs::remove_dir_all(entry.path());
1260− }
1261− }
1262−}
1263−
1264−/// The first path a checkout from `from` to `to` that skipped some left
1265−/// holding something other than what it writes there: with no directory
1266−/// above it, or a directory at it, or, where it adds the path, anything but
1267−/// what it writes (`holds_written`); none if no such path is found. Only
1268−/// once a checkout has skipped a path is the directory looked at again,
1269−/// reading the files it added until one differs.
1270−async fn first_skipped(
1271− root: &std::path::Path,
1272− markers: ConflictMarkerStyle,
1273− from: &MergedTree,
1274− to: &MergedTree,
1275−) -> Result<Option<RepoPathBuf>, String> {
1276− let mut dirs = HashMap::new();
1277− let mut diff = from.diff_stream(to, &EverythingMatcher);
1278− while let Some(entry) = diff.next().await {
1279− let Diff { before, after } = entry.values.map_err(|e| e.to_string())?;
1280− if after.is_absent() || matches!(after.as_normal(), Some(TreeValue::GitSubmodule(_))) {
1281− continue;
1282− }
1283− let path = entry.path;
1284− if let Some((_, stands)) = first_not_a_dir(root, &path, &mut dirs) {
1285− if stands.is_some() {
1286− return Ok(Some(path));
1287− }
1288− continue;
1289− }
1290− let Ok(disk) = path.to_fs_path(root) else {
1291− continue;
1292− };
1293− let Ok(meta) = disk.symlink_metadata() else {
1294− continue;
1295− };
1296− if meta.is_dir()
1297− || (before.is_absent() && !holds_written(to.store(), &path, after, to.labels(), markers, &disk).await?)
1298− {
1299− return Ok(Some(path));
1300− }
1301− }
1302− Ok(None)
1303−}
1304−
1305−/// Save the working copy's state as a scan that found `scanned` left it:
1306−/// at `head`, the operation the repository is at, where that is the
1307−/// working-copy commit's tree `wc`; otherwise at the operation it was
1308−/// saved at before, so that it stays stale for jj too, which takes a state
1309−/// saved at the head for a current one and would record the directory
1310−/// into the commit whole (§7.4)
1311−async fn save_scan(
1312− mut locked_ws: jj_lib::workspace::LockedWorkspace<'_>,
1313− scanned: &MergedTree,
1314− wc: &MergedTree,
1315− head: &OperationId,
1316−) -> Result<(), String> {
1317− let op_id = if scanned.tree_ids() == wc.tree_ids() {
1318− head.clone()
1319− } else {
1320− locked_ws.locked_wc().old_operation_id().clone()
1321− };
1322− locked_ws.finish(op_id).await.map_err(|e| e.to_string())
1323−}
1324−
1325−/// The signal that asked the run to stop while its checkout was not to be
1326−/// cut short (`Deferral`), or 0
1327−static DEFERRED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
1328−
1329−extern "C" fn defer(sig: libc::c_int) {
1330− DEFERRED.store(sig, std::sync::atomic::Ordering::SeqCst);
1331−}
1332−
1333−/// While it lives, SIGINT, SIGTERM, SIGHUP and SIGQUIT, whichever thread
1334−/// gets them, only note that they came: it lives from the operation's
1335−/// publishing until the working copy's state is saved, so that a checkout
1336−/// the user interrupts completes, leaving the working copy current, and
1337−/// through a clone, which one that comes before the working-copy commit is
1338−/// made fails (`unless_interrupted`), so that it removes what it made
1339−/// (§1.3). A signal the process ignores stays ignored, as it does for the
1340−/// git a clone runs (`nohup j clone`, a background job). Dropping it gives
1341−/// each signal back what it did before, and `raise_deferred` then acts on
1342−/// one that came.
1343−struct Deferral {
1344− previous: Vec<(libc::c_int, libc::sigaction)>,
1345−}
1346−
1347−impl Deferral {
1348− fn start() -> Deferral {
1349− let mut previous = Vec::new();
1350− for sig in [libc::SIGINT, libc::SIGTERM, libc::SIGHUP, libc::SIGQUIT] {
1351− // SAFETY: the handler only stores to an atomic, which is
1352− // async-signal-safe; both actions are initialised before use
1353− unsafe {
1354− let mut old: libc::sigaction = std::mem::zeroed();
1355− if libc::sigaction(sig, std::ptr::null(), &mut old) != 0
1356− || old.sa_sigaction == libc::SIG_IGN
1357− {
1358− continue;
1359− }
1360− let mut action: libc::sigaction = std::mem::zeroed();
1361− action.sa_sigaction = defer as extern "C" fn(libc::c_int) as libc::sighandler_t;
1362− action.sa_flags = libc::SA_RESTART;
1363− libc::sigemptyset(&mut action.sa_mask);
1364− if libc::sigaction(sig, &action, std::ptr::null_mut()) == 0 {
1365− previous.push((sig, old));
1366− }
1367− }
1368− }
1369− Deferral { previous }
1370− }
1371−}
1372−
1373−impl Drop for Deferral {
1374− fn drop(&mut self) {
1375− for (sig, old) in &self.previous {
1376− // SAFETY: `old` is the action `sigaction` gave back for `sig`
1377− unsafe { libc::sigaction(*sig, old, std::ptr::null_mut()) };
1378− }
1379− }
1380−}
1381−
1382−/// Act on a signal a checkout or a clone deferred (§1.3) as it would have
1383−/// acted then: with its default action, stopping the process (`Deferral`
1384−/// defers no signal the process ignores). Called once the run has reported
1385−/// what it did.
1386−pub fn raise_deferred() {
1387− let sig = DEFERRED.swap(0, std::sync::atomic::Ordering::SeqCst);
1388− if sig != 0 {
1389− // a process a signal stops does not flush what it printed
1390− use std::io::Write;
1391− let _ = std::io::stdout().flush();
1392− // SAFETY: raising a signal whose action `Deferral` gave back
1393− unsafe { libc::raise(sig) };
1394− }
1395−}
1396−
1397−/// `done`, the outcome of a step of a clone, unless a signal to stop came
1398−/// while `cmd_clone`'s `Deferral` held it off: the clone then fails, before
1399−/// it records anything, so that it removes what it made (§1.3, §7.8). A
1400−/// Ctrl-C at the terminal stops the git the step runs too, which fails the
1401−/// step itself: `j` may take the signal only after that, and then reports
1402−/// the step's failure.
1403−fn unless_interrupted<T>(done: Result<T, OpenError>) -> Result<T, OpenError> {
1404− if DEFERRED.load(std::sync::atomic::Ordering::SeqCst) != 0 {
1405− Err((1, "clone interrupted".to_string()))
1406− } else {
1407− done
1408− }
1409−}
1410−
1411−/// `commit` with `tree` in place of its own, for the working copy to read:
1412−/// a checkout and a reset read only a commit's tree. It is never written.
1413−fn with_tree(commit: &Commit, tree: MergedTree) -> Commit {
1414− let (tree_ids, labels) = tree.into_tree_ids_and_labels();
1415− Commit::new(
1416− commit.store().clone(),
1417− commit.id().clone(),
1418− Arc::new(jj_lib::backend::Commit {
1419− root_tree: tree_ids,
1420− conflict_labels: labels.into_merge(),
1421− ..commit.store_commit().as_ref().clone()
1422− }),
1423− )
1424}
275}
1425
276
1426#[derive(Clone)]
277#[derive(Clone)]
1427pub struct JjBackend {
278pub struct JjBackend {
1428 inner: Arc<JjInner>,
279 inner: Arc<JjInner>,
1429− replay_cache: Rc<ReplayCache>,
1430}
280}
1431
281
1432impl JjBackend {
282impl JjBackend {
@@ -1434,7 +284,7 @@ impl JjBackend {
1434 let root = find_repo_root().ok_or_else(|| {
284 let root = find_repo_root().ok_or_else(|| {
1435 (
285 (
1436 2,
286 2,
1437− "no jj repository found in this directory or any parent; run `j init` here, `j clone URL`, or `j help`".to_string(),
287+ "no jj repository found in this directory or any parent".to_string(),
1438 )
288 )
1439 })?;
289 })?;
1440 let settings = user_settings(cfg);
290 let settings = user_settings(cfg);
@@ -1455,22 +305,10 @@ impl JjBackend {
1455 ),
305 ),
1456 ));
306 ));
1457 }
307 }
1458− // Snapshot and checkout both apply patterns saved by `jj sparse`,
1459− // independently of our SnapshotOptions. Accepting a partial working
1460− // copy would silently omit disk edits and skip files during checkout.
1461− let sparse = workspace.working_copy().sparse_patterns()
1462− .map_err(|e| (2, format!("cannot read the working copy's sparse patterns: {}", e)))?;
1463− if !sparse.iter().any(|path| path.is_root()) {
1464− return Err((
1465− 2,
1466− "sparse working copies are not supported; run `jj sparse set --clear --add .` to include all files before running j".to_string(),
1467− ));
1468− }
1469 let workspace_name = workspace.workspace_name().to_owned();
308 let workspace_name = workspace.workspace_name().to_owned();
1470 let repo = block_on(workspace.repo_loader().load_at_head())
309 let repo = block_on(workspace.repo_loader().load_at_head())
1471 .map_err(|e| (2, format!("cannot load the repository at head: {}", e)))?;
310 .map_err(|e| (2, format!("cannot load the repository at head: {}", e)))?;
1472 Ok(JjBackend {
311 Ok(JjBackend {
1473− replay_cache: Rc::new(ReplayCache::default()),
1474 inner: Arc::new(JjInner {
312 inner: Arc::new(JjInner {
1475 workspace_root: root,
313 workspace_root: root,
1476 workspace: Mutex::new(workspace),
314 workspace: Mutex::new(workspace),
@@ -1479,8 +317,6 @@ impl JjBackend {
1479 visible: Mutex::new(None),
317 visible: Mutex::new(None),
1480 lock_guard: Mutex::new(None),
318 lock_guard: Mutex::new(None),
1481 pending: Mutex::new(None),
319 pending: Mutex::new(None),
1482− folds_case: OnceLock::new(),
1483− long_names: Mutex::new(HashMap::new()),
1484 }),
320 }),
1485 })
321 })
1486 }
322 }
@@ -1500,22 +336,21 @@ impl JjBackend {
1500 {
336 {
1501 Ok(f) => f,
337 Ok(f) => f,
1502 Err(e) => {
338 Err(e) => {
1503− crate::terminal::report_error(format_args!("j: cannot create {}: {}", lock_path.display(), e));
339+ eprintln!("j: cannot create {}: {}", lock_path.display(), e);
1504 std::process::exit(2);
340 std::process::exit(2);
1505 }
341 }
1506 };
342 };
1507 let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
343 let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
1508 if rc != 0 {
344 if rc != 0 {
1509− crate::terminal::report_error(format_args!("j: another j is running in this repository"));
345+ eprintln!("j: another j is running in this repository");
1510 std::process::exit(2);
346 std::process::exit(2);
1511 }
347 }
1512 *self.inner.lock_guard.lock().unwrap() = Some(FileLock(file));
348 *self.inner.lock_guard.lock().unwrap() = Some(FileLock(file));
1513− put_back_left(&self.inner.workspace_root);
1514 }
349 }
1515
350
1516 pub fn build_interp(
351 pub fn build_interp(
1517 &self,
352 &self,
1518− cfg: &mut Config,
353+ cfg: &Config,
1519 _text: &str,
354 _text: &str,
1520 snapshot: bool,
355 snapshot: bool,
1521 ) -> Result<(Interp, Value, Value), OpenError> {
356 ) -> Result<(Interp, Value, Value), OpenError> {
@@ -1524,8 +359,8 @@ impl JjBackend {
1524 let head_repo = block_on(self.inner.workspace.lock().unwrap().repo_loader().load_at_head())
359 let head_repo = block_on(self.inner.workspace.lock().unwrap().repo_loader().load_at_head())
1525 .map_err(|e| (2, format!("cannot reload the repository at head: {}", e)))?;
360 .map_err(|e| (2, format!("cannot reload the repository at head: {}", e)))?;
1526 *self.inner.repo.lock().unwrap() = head_repo;
361 *self.inner.repo.lock().unwrap() = head_repo;
1527− let loaded_visible = Arc::new(block_on(read_visible(&self.current_repo()))?);
362+ let loaded_visible = block_on(read_visible(&self.current_repo()))?;
1528− let loaded = build_repo_value(&loaded_visible, &loaded_visible.wc_change_id, &self.replay_cache)?;
363+ let loaded = build_repo_value(&loaded_visible, &loaded_visible.wc_change_id)?;
1529
364
1530 let current = if snapshot {
365 let current = if snapshot {
1531 // §7.4: snapshot the working directory into the wc commit; the
366 // §7.4: snapshot the working directory into the wc commit; the
@@ -1536,19 +371,19 @@ impl JjBackend {
1536 // no working-copy change: the repo is the one already loaded,
371 // no working-copy change: the repo is the one already loaded,
1537 // so reuse its value instead of rebuilding it
372 // so reuse its value instead of rebuilding it
1538 None => {
373 None => {
1539− *self.inner.visible.lock().unwrap() = Some(loaded_visible.clone());
374+ *self.inner.visible.lock().unwrap() = Some(Arc::new(loaded_visible));
1540 loaded.clone()
375 loaded.clone()
1541 }
376 }
1542 Some(pending) => {
377 Some(pending) => {
1543 *self.inner.pending.lock().unwrap() = Some(pending);
378 *self.inner.pending.lock().unwrap() = Some(pending);
1544 let vis = block_on(read_visible(&post))?;
379 let vis = block_on(read_visible(&post))?;
1545− let cur = build_repo_value(&vis, &vis.wc_change_id, &self.replay_cache)?;
380+ let cur = build_repo_value(&vis, &vis.wc_change_id)?;
1546 *self.inner.visible.lock().unwrap() = Some(Arc::new(vis));
381 *self.inner.visible.lock().unwrap() = Some(Arc::new(vis));
1547 cur
382 cur
1548 }
383 }
1549 }
384 }
1550 } else {
385 } else {
1551− *self.inner.visible.lock().unwrap() = Some(loaded_visible.clone());
386+ *self.inner.visible.lock().unwrap() = Some(Arc::new(loaded_visible));
1552 loaded.clone()
387 loaded.clone()
1553 };
388 };
1554
389
@@ -1557,52 +392,27 @@ impl JjBackend {
1557 cfg.shapes.clone(),
392 cfg.shapes.clone(),
1558 crate::value::Env::empty(),
393 crate::value::Env::empty(),
1559 );
394 );
1560− // §1.2.5: resolve the config's `@prefix` literals now that the visible
1561− // ids are known, and before anything evaluates a definition
1562− if let Some((def, prefix, _)) =
1563− crate::config::resolve_config_ids(cfg, &interp).into_iter().next()
1564− {
1565− return Err((
1566− 3,
1567− format!(
1568− "config.j: `@{}` in `{}` does not resolve to a unique commit",
1569− prefix, def
1570− ),
1571− ));
1572− }
1573 // §7.2: if the focused commit is in the immutable set, the snapshot
395 // §7.2: if the focused commit is in the immutable set, the snapshot
1574 // cannot go into it; the focus is a new empty child of it holding the
396 // cannot go into it; the focus is a new empty child of it holding the
1575 // snapshot, and persisting records that child
397 // snapshot, and persisting records that child
1576− match self.focus_if_immutable(&mut interp, cfg, &loaded, &current)? {
398+ match self.focus_if_immutable(&mut interp, cfg, &current)? {
1577− Some((loaded, current)) => {
399+ Some(adjusted) => {
1578− // the snapshot's rewrite of the wc commit (and its rebase of
400+ // the persisted child is part of the loaded value too, so a
1579− // the descendants) is not the repository's: lookups answer
401+ // run that changes nothing is a no-op
1580− // from the stored commits, and persisting leaves the wc
402+ Ok((interp, adjusted.clone(), adjusted))
1581− // commit as stored rather than folding the snapshot into it
1582− *self.inner.visible.lock().unwrap() = Some(loaded_visible);
1583− if let Some(p) = self.inner.pending.lock().unwrap().as_mut() {
1584− p.fold_into_wc = false;
1585− }
1586− Ok((interp, loaded, current))
1587 }
403 }
1588 None => Ok((interp, loaded, current)),
404 None => Ok((interp, loaded, current)),
1589 }
405 }
1590 }
406 }
1591
407
1592− /// If the focus of `current` is immutable, return the loaded and current
408+ /// If the focus of `current` is immutable, return a repo refocused on a
1593− /// repos refocused on a new empty child of it (§7.2). Both are `loaded`
409+ /// new empty child holding the focus's files (§7.2).
1594− /// with that one child added: the snapshot goes into neither the focus
1595− /// nor, through a rebase, its descendants. The child holds the focus's
1596− /// stored files in the loaded repo and the snapshot in the current one,
1597− /// so a dirty `j id` differs from the loaded value and records the child
1598− /// (§1.2 step 8).
1599 fn focus_if_immutable(
410 fn focus_if_immutable(
1600 &self,
411 &self,
1601 interp: &mut Interp,
412 interp: &mut Interp,
1602 cfg: &Config,
413 cfg: &Config,
1603− loaded: &Value,
1604 current: &Value,
414 current: &Value,
1605− ) -> Result<Option<(Value, Value)>, OpenError> {
415+ ) -> Result<Option<Value>, OpenError> {
1606 // Evaluate the config to compute the immutable set. This uses the
416 // Evaluate the config to compute the immutable set. This uses the
1607 // interpreter the run will go on to use, rather than a throwaway one,
417 // interpreter the run will go on to use, rather than a throwaway one,
1608 // so that the `immutable` (and the `trunk` inside it) computed here is
418 // so that the `immutable` (and the `trunk` inside it) computed here is
@@ -1618,101 +428,89 @@ impl JjBackend {
1618 if !immutable.contains(&focus_id) {
428 if !immutable.contains(&focus_id) {
1619 return Ok(None);
429 return Ok(None);
1620 }
430 }
1621− // the focus's location in `loaded`, pre-snapshot; the child is its
431+ // build: new empty child of the focus with the focus's files, minted id
1622− // last child, so every existing child sits to the left of it
1623− let field = |v: &Value, name: &str| v.field(name).map_err(|c| (1, c.msg));
432+ let root = current.field("root").map_err(|c| (1, c.msg))?;
1624− let root = field(loaded, "root")?;
433+ let files = root.field("files").map_err(|c| (1, c.msg))?;
1625− let frame = Value::record(&[
434+ let child = crate::value::Value::record(&[
1626− ("left", field(loaded, "children")?),
435+ ("files", files),
1627− ("parent", root.clone()),
436+ ("message", crate::value::Value::text("")),
1628− ("right", Value::list(vec![])),
437+ ("labels", crate::value::Value::list(vec![])),
438+ (
439+ "id",
440+ crate::value::Value::Id(Rc::new(interp.mint_id())),
441+ ),
1629 ]);
442 ]);
1630− let mut frames = vec![frame];
443+ let child_subtree = crate::value::Value::record(&[
1631− frames.extend_from_slice(field(loaded, "context")?.as_list().map_err(|c| (1, c.msg))?);
444+ ("root", child.clone()),
1632− let context = Value::list(frames);
445+ ("children", crate::value::Value::list(vec![])),
1633− let id = Value::Id(Rc::new(interp.mint_id()));
446+ ]);
1634− let at_child = |files: Value| {
447+ let mut new_children: Vec<Value> = current
1635− Value::record(&[
448+ .field("children")
1636− ("children", Value::list(vec![])),
449+ .map_err(|c| (1, c.msg))?
1637− ("context", context.clone()),
450+ .as_list()
1638− (
451+ .map_err(|c| (1, c.msg))?
1639− "root",
452+ .to_vec();
1640− Value::record(&[
453+ new_children.push(child_subtree);
1641− ("files", files),
454+ let parent = crate::value::Value::record(&[
1642− ("id", id.clone()),
455+ ("children", crate::value::Value::list(new_children)),
1643− ("labels", Value::list(vec![])),
456+ ("context", current.field("context").map_err(|c| (1, c.msg))?),
1644− ("message", Value::text("")),
457+ ("root", root),
1645− ]),
458+ ]);
1646− ),
459+ // refocus onto the child (it is the last child)
1647− ])
460+ let child_id = match child.field("id").map_err(|c| (1, c.msg))? {
1648− };
461+ Value::Id(i) => i.to_string(),
1649− let current_at_child = at_child(field(&field(current, "root")?, "files")?);
462+ _ => unreachable!(),
1650− // with nothing snapshotted `current` is `loaded` itself: share one
1651− // value, so a run that changes nothing is a no-op without comparing
1652− // the whole repository
1653− let loaded_at_child = match (loaded, current) {
1654− (Value::Record(a), Value::Record(b)) if Rc::ptr_eq(a, b) => current_at_child.clone(),
1655− _ => at_child(field(&root, "files")?),
1656 };
463 };
1657− Ok(Some((loaded_at_child, current_at_child)))
464+ crate::repo::by_id(&parent, &child_id)
465+ .map_err(|c| (1, c.msg))?
466+ .ok_or_else(|| (1, "internal: cannot refocus".to_string()))
467+ .map(Some)
1658 }
468 }
1659
469
1660− /// Snapshot the working directory into the working-copy commit (§7.4);
470+ /// Snapshot the working copy into the wc commit; returns the repo at the
1661− /// return the repo at the snapshot's operation, written but not
471+ /// snapshot operation (unpublished but written), or the pre-snapshot repo
1662− /// published, or the repo as loaded when there is nothing to record.
472+ /// if nothing changed.
1663 async fn snapshot_repo(
473 async fn snapshot_repo(
1664 &self,
474 &self,
1665 ) -> Result<(Arc<ReadonlyRepo>, Option<PendingSnapshot>), OpenError> {
475 ) -> Result<(Arc<ReadonlyRepo>, Option<PendingSnapshot>), OpenError> {
1666 let mut ws_guard = self.inner.workspace.lock().unwrap();
476 let mut ws_guard = self.inner.workspace.lock().unwrap();
1667− let root = ws_guard.workspace_root().to_owned();
477+ let loader = ws_guard.repo_loader().clone();
1668− let saved = SavedState::of(&ws_guard);
1669 let mut locked_ws = ws_guard.start_working_copy_mutation()
478 let mut locked_ws = ws_guard.start_working_copy_mutation()
1670 .await
479 .await
1671 .map_err(|e| (2, format!("cannot lock the working copy: {}", e)))?;
480 .map_err(|e| (2, format!("cannot lock the working copy: {}", e)))?;
1672− let old_tree = locked_ws.locked_wc().old_tree().clone();
481+ let options = SnapshotOptions {
1673− let scanned = scan(locked_ws.locked_wc(), &root, &old_tree, &saved, &snapshot_options())
482+ base_ignores: GitIgnoreFile::empty(),
483+ progress: None,
484+ start_tracking_matcher: &EverythingMatcher,
485+ force_tracking_matcher: &NothingMatcher,
486+ max_new_file_size: u64::MAX,
487+ };
488+ let (new_tree, _stats) = locked_ws
489+ .locked_wc()
490+ .snapshot(&options)
1674 .await
491 .await
1675 .map_err(|e| (2, format!("cannot snapshot the working copy: {}", e)))?;
492 .map_err(|e| (2, format!("cannot snapshot the working copy: {}", e)))?;
1676− // the repo build_interp loaded at head from the workspace's own
493+ let changed = new_tree.tree_ids() != locked_ws.locked_wc().old_tree().tree_ids();
1677− // loader, whose store the scanned tree lives in
494+ if !changed {
1678− let head = self.current_repo();
495+ locked_ws
1679− let wc_commit = self.wc_commit(&head).await?;
496+ .finish(self.current_repo().operation().id().clone())
1680− let current = locked_ws.locked_wc().old_operation_id() == head.operation().id();
1681− let tree = match marker_style(head.settings()) {
1682− Ok(markers) => snapshot_tree(&root, markers, &wc_commit.tree(), &old_tree, current, &scanned).await,
1683− Err(e) => Err(e),
1684− }
1685− .map_err(|e| (2, format!("cannot snapshot the working copy: {}", e)))?;
1686− if tree.tree_ids() == wc_commit.tree().tree_ids() {
1687− // nothing to record: the directory holds the commit's files, or,
1688− // where the state is stale, the state's, or changes to them the
1689− // commit already has, such as what a checkout cut short wrote.
1690− // What was scanned is saved, at the operation the state is
1691− // stale since where it still is.
1692− save_scan(locked_ws, &scanned, &wc_commit.tree(), head.operation().id())
1693 .await
497 .await
1694 .map_err(|e| (2, format!("cannot finish the snapshot: {}", e)))?;
498 .map_err(|e| (2, format!("cannot finish the snapshot: {}", e)))?;
1695− return Ok((head, None));
499+ return Ok((self.current_repo(), None));
1696 }
500 }
1697− // a directory come to hold the parent's files, as where it reverts
501+ // the snapshotted tree lives in the workspace's store; drive the
1698− // the commit's change, is recorded with the parent's tree (§7.4), as
502+ // transaction from the workspace's repo loader so stores match
1699− // persistence writes a commit with its parent's files (§7.5 step 4):
503+ let ws_repo = loader
1700− // the snapshot builds on the commit's tree, and so could make
504+ .load_at_head()
1701− // another tree for the parent's files, one jj takes for a change.
505+ .await
1702− // Not before the check above, or a commit already holding such a
506+ .map_err(|e| (2, format!("cannot reload the repository: {}", e)))?;
1703− // tree would be rewritten by a directory that has not changed. A
1704− // merge is immutable, so never folded into (§7.2), and its parents'
1705− // trees are not merged for it
1706− let tree = match wc_commit.parent_ids() {
1707− [_] => parents_tree_if_same(head.as_ref(), wc_commit.parent_ids(), tree).await,
1708− _ => tree,
1709− };
1710− let mut tx = head.start_transaction();
507+ let mut tx = ws_repo.start_transaction();
1711 tx.set_is_snapshot(true);
508 tx.set_is_snapshot(true);
509+ let wc_commit = self.wc_commit(&ws_repo).await?;
1712 let new_wc = tx
510 let new_wc = tx
1713 .repo_mut()
511 .repo_mut()
1714 .rewrite_commit(&wc_commit)
512 .rewrite_commit(&wc_commit)
1715− .set_tree(tree)
513+ .set_tree(new_tree)
1716 .write()
514 .write()
1717 .await
515 .await
1718 .map_err(|e| (2, format!("cannot write the snapshot commit: {}", e)))?;
516 .map_err(|e| (2, format!("cannot write the snapshot commit: {}", e)))?;
@@ -1729,6 +527,7 @@ impl JjBackend {
1729 .write("snapshot working copy")
527 .write("snapshot working copy")
1730 .await
528 .await
1731 .map_err(|e| (2, format!("cannot write the snapshot operation: {}", e)))?;
529 .map_err(|e| (2, format!("cannot write the snapshot operation: {}", e)))?;
530+ let op_id = unpublished.operation().id().clone();
1732 let new_repo = unpublished.leave_unpublished();
531 let new_repo = unpublished.leave_unpublished();
1733 // Release the lock *without* finishing: `finish` saves the scanned
532 // Release the lock *without* finishing: `finish` saves the scanned
1734 // tree state to disk, which would record the working directory as
533 // tree state to disk, which would record the working directory as
@@ -1736,19 +535,20 @@ impl JjBackend {
1736 // tree. The next run's incremental snapshot would then find nothing
535 // tree. The next run's incremental snapshot would then find nothing
1737 // to do and build the Repo from the stale tree — and a persisting run
536 // to do and build the Repo from the stale tree — and a persisting run
1738 // would check that stale tree back out, destroying the edits. The
537 // would check that stale tree back out, destroying the edits. The
1739− // state is advanced only by `persist`'s checkout, which first scans
538+ // state is advanced only by `persist`'s checkout, which finishes
1740− // the directory again and finishes against the operation that
1741− // actually recorded the tree (§7.4/§7.7).
539+ // against the operation that actually recorded the tree (§7.4/§7.7).
1742 drop(locked_ws);
540 drop(locked_ws);
541+ let _ = op_id;
1743 let pending = PendingSnapshot {
542 let pending = PendingSnapshot {
1744− pre_repo: head,
543+ pre_repo: self.current_repo(),
544+ new_wc_id: new_wc.id().clone(),
1745 tree: new_wc.tree().clone(),
545 tree: new_wc.tree().clone(),
1746− fold_into_wc: true,
1747− scanned,
1748 };
546 };
1749 Ok((new_repo, Some(pending)))
547 Ok((new_repo, Some(pending)))
1750 }
548 }
1751
549
550+
551+
1752 async fn wc_commit(&self, repo: &Arc<ReadonlyRepo>) -> Result<Commit, OpenError> {
552 async fn wc_commit(&self, repo: &Arc<ReadonlyRepo>) -> Result<Commit, OpenError> {
1753 let id = repo
553 let id = repo
1754 .view()
554 .view()
@@ -1766,13 +566,12 @@ impl JjBackend {
1766 interp: &mut Interp,
566 interp: &mut Interp,
1767 cfg: &Config,
567 cfg: &Config,
1768 loaded: &Value,
568 loaded: &Value,
1769− current: &Value,
569+ _current: &Value,
1770 new: &Value,
570 new: &Value,
1771 text: &str,
571 text: &str,
1772− ) -> Result<bool, Crash> {
572+ ) -> Result<(), Crash> {
1773− // §1.2 step 8: unchanged, up to the order of snapshot entries (§7.3)
1774− if crate::repo::same_repo(loaded, new)? {
573+ if crate::value::value_eq(loaded, new)? {
1775− return Ok(false);
574+ return Ok(());
1776 }
575 }
1777 *interp.old_repo.borrow_mut() = Some(loaded.clone());
576 *interp.old_repo.borrow_mut() = Some(loaded.clone());
1778 crate::repo::validate_repo(interp, new)?;
577 crate::repo::validate_repo(interp, new)?;
@@ -1792,7 +591,7 @@ impl JjBackend {
1792 // walk below compares against its output, so the snapshot change and
591 // walk below compares against its output, so the snapshot change and
1793 // the expression's edits become one rewrite
592 // the expression's edits become one rewrite
1794 let mut snapshot_wc: Option<Commit> = None;
593 let mut snapshot_wc: Option<Commit> = None;
1795− if let Some(p) = pending.as_ref().filter(|p| p.fold_into_wc) {
594+ if let Some(p) = &pending {
1796 let wc_commit = block_on(self.wc_commit(&base)).map_err(|e| Crash::new(e.1))?;
595 let wc_commit = block_on(self.wc_commit(&base)).map_err(|e| Crash::new(e.1))?;
1797 let c = block_on(
596 let c = block_on(
1798 tx.repo_mut()
597 tx.repo_mut()
@@ -1816,9 +615,6 @@ impl JjBackend {
1816 old_stored = Arc::new(vis);
615 old_stored = Arc::new(vis);
1817 }
616 }
1818 let mut written: BTreeMap<String, CommitId> = BTreeMap::new();
617 let mut written: BTreeMap<String, CommitId> = BTreeMap::new();
1819− // the stored trees the program's commits were loaded with
1820− let visible = self.inner.visible.lock().unwrap().clone();
1821− let loaded_trees = LoadedTrees::new(visible.as_deref(), &[current, new])?;
1822 // shared across the whole persist walk so unchanged blobs are inflated once
618 // shared across the whole persist walk so unchanged blobs are inflated once
1823 let cache = BlobCache::default();
619 let cache = BlobCache::default();
1824 let entries = EntryCache::default();
620 let entries = EntryCache::default();
@@ -1842,35 +638,33 @@ impl JjBackend {
1842 }
638 }
1843 let stored_files = block_on(tree_to_files(&store, &root_jj.tree(), &cache, &entries))
639 let stored_files = block_on(tree_to_files(&store, &root_jj.tree(), &cache, &entries))
1844 .map_err(|e| Crash::new(e.msg))?;
640 .map_err(|e| Crash::new(e.msg))?;
641+ if !crate::value::value_eq(
1845− if !crate::repo::snapshot_eq(&Value::list(stored_files), &root_v.field("files")?)? {
642+ &Value::list(stored_files),
643+ &Value::list(root_v.field("files")?.as_list()?.to_vec()),
644+ )? {
1846 return Err(Crash::new("persistence: the root commit cannot be changed"));
645 return Err(Crash::new("persistence: the root commit cannot be changed"));
1847 }
646 }
1848 }
647 }
1849 written.insert(ROOT_ID.to_string(), root_jj.id().clone());
648 written.insert(ROOT_ID.to_string(), root_jj.id().clone());
1850
649
1851 // walk `new` top-down (§7.5 step 4), parents before children
650 // walk `new` top-down (§7.5 step 4), parents before children
1852− // a deque, not a Vec: `remove(0)` shifts everything after it, which
1853− // is quadratic in the number of commits for a wide history
1854− let mut queue: std::collections::VecDeque<(Value, String)> =
1855− std::collections::VecDeque::new(); // (subtree value, parent change id)
651+ let mut queue: Vec<(Value, String)> = Vec::new(); // (subtree value, parent change id)
1856 for kid in top.field("children")?.as_list()?.iter() {
652 for kid in top.field("children")?.as_list()?.iter() {
1857− queue.push_back((kid.clone(), ROOT_ID.to_string()));
653+ queue.push((kid.clone(), ROOT_ID.to_string()));
1858 }
654 }
655+ while !queue.is_empty() {
1859− while let Some((subtree, parent_change)) = queue.pop_front() {
656+ let (subtree, parent_change) = queue.remove(0);
1860 let commit_v = subtree.field("root")?;
657 let commit_v = subtree.field("root")?;
1861 let id = commit_id_of(&commit_v)?;
658 let id = commit_id_of(&commit_v)?;
1862 let parent_jj = written
659 let parent_jj = written
1863 .get(&parent_change)
660 .get(&parent_change)
1864 .cloned()
661 .cloned()
1865 .ok_or_else(|| Crash::new("persistence: internal: parent not yet written"))?;
662 .ok_or_else(|| Crash::new("persistence: internal: parent not yet written"))?;
1866− // not read unless compared or written: most commits keep the
663+ let files_v = commit_v.field("files")?;
1867− // stored tree they were loaded with
1868− let files_v = crate::repo::files_of(&commit_v)?;
1869 let message = commit_v.field("message")?.as_text()?.to_string();
664 let message = commit_v.field("message")?.as_text()?.to_string();
1870 let new_id = match old_stored.commits.get(&id) {
665 let new_id = match old_stored.commits.get(&id) {
1871 None => {
666 None => {
1872− let tree =
667+ let tree = block_on(build_tree(&store, &files_v))?;
1873− loaded_trees.tree_to_write(tx.repo(), &files_v, std::slice::from_ref(&parent_jj))?;
1874 let change_id = jj_lib::backend::ChangeId::try_from_reverse_hex(&id).ok_or_else(|| {
668 let change_id = jj_lib::backend::ChangeId::try_from_reverse_hex(&id).ok_or_else(|| {
1875 Crash::new(format!("persistence: `{}` is not a valid change id", id))
669 Crash::new(format!("persistence: `{}` is not a valid change id", id))
1876 })?;
670 })?;
@@ -1889,31 +683,17 @@ impl JjBackend {
1889 Some(stored) => {
683 Some(stored) => {
1890 let stored_commit = &stored.commit;
684 let stored_commit = &stored.commit;
1891 let parent_changed = hex_of(stored_commit.parent_ids().first()) != parent_jj.hex();
685 let parent_changed = hex_of(stored_commit.parent_ids().first()) != parent_jj.hex();
1892− let stored_tree = stored_commit.tree();
1893− // loaded from the stored tree, the files are unchanged:
1894− // reading every commit's to compare them made each
1895− // persist read the files of the whole history
1896− let files_changed = crate::repo::stored_tree(&files_v)
686+ let files_changed = {
1897− != Some(tree_name(&stored_tree).as_str())
1898− && {
1899− let stored_files =
687+ let stored_files =
1900− block_on(tree_to_files(&store, &stored_tree, &cache, &entries))?;
688+ block_on(tree_to_files(&store, &stored_commit.tree(), &cache, &entries))?;
1901− // a tree has no order: entries listed differently
689+ !crate::value::value_eq(
1902− // are not a change to rewrite (§7.3)
1903− !crate::repo::snapshot_eq(&Value::list(stored_files), &files_v)?
690+ &Value::list(stored_files),
691+ &Value::list(files_v.as_list()?.to_vec()),
692+ )?
1904− };
693+ };
1905 let msg_changed = stored_commit.description() != message;
694 let msg_changed = stored_commit.description() != message;
1906 if parent_changed || files_changed || msg_changed {
695 if parent_changed || files_changed || msg_changed {
1907− // files the stored tree lists are written as it is,
696+ let tree = block_on(build_tree(&store, &files_v))?;
1908− // but where they are the parent's, as the files of a
1909− // commit moved onto another tree for them come to be,
1910− // with the parent's tree (§7.5 step 4)
1911− let parents = std::slice::from_ref(&parent_jj);
1912− let tree = if files_changed {
1913− loaded_trees.tree_to_write(tx.repo(), &files_v, parents)?
1914− } else {
1915− block_on(parents_tree_if_same(tx.repo(), parents, stored_tree))
1916− };
1917 let c = block_on(
697 let c = block_on(
1918 tx.repo_mut()
698 tx.repo_mut()
1919 .rewrite_commit(stored_commit)
699 .rewrite_commit(stored_commit)
@@ -1932,7 +712,7 @@ impl JjBackend {
1932 };
712 };
1933 written.insert(id.clone(), new_id);
713 written.insert(id.clone(), new_id);
1934 for kid in subtree.field("children")?.as_list()?.iter() {
714 for kid in subtree.field("children")?.as_list()?.iter() {
1935− queue.push_back((kid.clone(), id.clone()));
715+ queue.push((kid.clone(), id.clone()));
1936 }
716 }
1937 }
717 }
1938
718
@@ -1976,230 +756,37 @@ impl JjBackend {
1976 let desc = truncate_chars(text, 200);
756 let desc = truncate_chars(text, 200);
1977 let unpublished = block_on(tx.write(desc))
757 let unpublished = block_on(tx.write(desc))
1978 .map_err(|e| Crash::new(format!("cannot write the operation: {}", e)))?;
758 .map_err(|e| Crash::new(format!("cannot write the operation: {}", e)))?;
759+ let op_id = unpublished.operation().id().clone();
760+ let _new_repo = block_on(unpublished.publish())
761+ .map_err(|e| Crash::new(format!("cannot publish the operation: {}", e)))?;
1979
762
1980− // §7.4/§7.5 step 7: publish the operation, then check the focus out
763+ // §7.4/§7.5 step 7: check out the focus to the working directory
1981 let focus_commit = block_on(store.get_commit_async(&focus_jj))
764 let focus_commit = block_on(store.get_commit_async(&focus_jj))
1982 .map_err(|e| Crash::new(format!("cannot read the focus commit: {}", e)))?;
765 .map_err(|e| Crash::new(format!("cannot read the focus commit: {}", e)))?;
1983− // the checkout starts from what the working directory holds: the
1984− // tree the run's snapshot scanned, or, where it had nothing to
1985− // record, the tree the working-copy state records, as
1986− // `snapshot_repo` left it. That is the focus as loaded unless the
1987− // state is stale (§7.4), when the checkout writes the focus over it;
1988− // taking the directory to hold the focus instead, it would find it
1989− // did not, describe the focus without writing it (a reset), and the
1990− // next run would record the stale files into the focus.
1991− let on_disk = match pending {
1992− Some(p) => p.scanned,
1993− None => {
1994− let state = self.inner.workspace.lock().unwrap().working_copy().tree().cloned();
1995− // a state that cannot be read fails the checkout, which
1996− // reads it again when it locks
1997− state.or_else(|_| block_on(self.wc_commit(&base)).map(|c| c.tree()).map_err(|e| Crash::new(e.1)))?
1998− }
1999− };
2000− block_on(self.checkout(&focus_commit, Some(&on_disk), Some(unpublished), Some(", and `j undo` goes back")))?;
766+ block_on(self.checkout(&focus_commit, op_id))?;
767+
2001− Ok(true)
768+ Ok(())
2002 }
769 }
2003
770
2004− /// Check `commit` out: write its files to the working directory and
2005− /// save the working copy's state as holding them (§7.4). `op`, the
2006− /// operation that makes it the working-copy commit, is published first,
2007− /// as jj does (§7.5 step 7); with none, `commit` is the head's own.
2008− /// `on_disk` is the tree the directory holds, which the checkout starts
2009− /// from: what a persisting run's snapshot scanned, or the tree the
2010− /// working-copy state records where it scanned nothing to record, or
2011− /// what `undo`'s look found; with none, the state as it is. A change
2012− /// made to the directory since `on_disk` was scanned, where the
2013− /// checkout writes, refuses the checkout before `op` is published
2014− /// (`written_over`); one made once it has begun is written over, as by
2015− /// jj. Where the commit adds a file, something `on_disk` does not track
2016− /// standing there, but for empty directories and what the checkout
2017− /// writes, refuses the checkout before `op` is published
2018− /// (`look_where_added`), and one put there since fails it after, as it
2019− /// makes jj skip the path. A checkout that fails, or is cut short, after
2020− /// `op` is published leaves `op` recorded and the state stale, recording
2021− /// `on_disk`: the next run takes what the checkout wrote for the
2022− /// commit's own and carries on (`snapshot_tree`), as the crash says,
2023− /// with `back` naming the way back; with no `back` it says nothing of
2024− /// the operation (a clone that fails removes it).
2025− async fn checkout(
771+ async fn checkout(&self, commit: &Commit, op_id: OperationId) -> Result<(), Crash> {
2026− &self,
2027− commit: &Commit,
2028− on_disk: Option<&MergedTree>,
2029− op: Option<UnpublishedOperation>,
2030− back: Option<&str>,
2031− ) -> Result<(), Crash> {
2032 let mut ws_guard = self.inner.workspace.lock().unwrap();
772 let mut ws_guard = self.inner.workspace.lock().unwrap();
2033− let root = ws_guard.workspace_root().to_owned();
2034− let saved = SavedState::of(&ws_guard);
2035 let mut locked_ws = ws_guard.start_working_copy_mutation()
773 let mut locked_ws = ws_guard.start_working_copy_mutation()
2036 .await
774 .await
2037 .map_err(|e| Crash::new(format!("cannot lock the working copy: {}", e)))?;
775 .map_err(|e| Crash::new(format!("cannot lock the working copy: {}", e)))?;
2038− // A persisting run's snapshot released its lock without saving what
776+ locked_ws
2039− // it scanned, so the state just loaded may still describe the tree
777+ .locked_wc()
2040− // recorded before the edits, and jj checks out by diffing that tree
778+ .check_out(commit)
2041− // against the new one: a path whose recorded content equals the
2042− // focus's would keep the user's edit, a file created since would
2043− // stay, and a file replaced by a directory (or back) would leave
2044− // state contradicting the tree. Scan the directory again first, as
2045− // the snapshot did, so the checkout diffs from what is actually on
2046− // disk (§7.4). A scan keeps what the state knows of each file where
2047− // a reset to the snapshot's tree would forget it: a conflict whose
2048− // sides hold marker-like lines is materialized with longer markers,
2049− // and a forgotten length makes the next snapshot read those markers
2050− // back at 7 characters, fail to parse them and record the edited
2051− // conflict as resolved text, markers and all (§7.4).
2052− if let Some(on_disk) = on_disk {
2053− let old_tree = locked_ws.locked_wc().old_tree().clone();
2054− let reset = match scan(locked_ws.locked_wc(), &root, &old_tree, &saved, &snapshot_options()).await {
2055− Ok(scanned) if scanned.tree_ids() == on_disk.tree_ids() => false,
2056− // the directory changed while the program ran: where the
2057− // checkout would write the focus over such a change, which
2058− // no snapshot holds and jj's checkout does not look for,
2059− // the run crashes before anything is recorded or written
2060− // (§7.5 step 7)
2061− Ok(scanned) => match written_over(on_disk, &scanned, &commit.tree()).await {
2062− Ok(None) => true,
2063− Ok(Some(path)) => {
2064− return Err(Crash::new(format!(
2065− "cannot check out the focus: `{}` changed in the working directory while the program ran, and the checkout would write over it; nothing was recorded, so run the command again",
2066− path.as_internal_file_string()
2067− )));
2068− }
2069− Err(e) => return Err(Crash::new(format!("cannot check out the focus: {}", e))),
2070− },
2071− // or it can no longer be scanned (an entry appeared,
2072− // vanished or cannot be read; a failed scan leaves the state
2073− // as it was)
2074− Err(_) => true,
2075− };
2076− // Otherwise the state describes `on_disk` instead. A reset
2077− // writes no file; the paths it changes are re-read by the next
2078− // run.
2079− if reset {
2080− locked_ws
2081− .locked_wc()
2082− .reset(&with_tree(commit, on_disk.clone()))
2083− .await
779+ .await
2084− .map_err(|e| Crash::new(format!("cannot reset the working copy: {}", e)))?;
2085− }
2086− }
2087− // Where the focus adds a file and something the directory does not
2088− // track stands (an ignored file, a symlink, a directory holding
2089− // one), jj's checkout would skip the path, and the next run read
2090− // what stands there into the focus: refuse before anything is
2091− // recorded or written, as git does (§7.5 step 7). The trees of
2092− // empty directories found there, and the files holding what the
2093− // checkout writes, are cleared once `op` is published.
2094− let markers = marker_style(self.current_repo().settings())
2095 .map_err(|e| Crash::new(format!("cannot check out the focus: {}", e)))?;
780 .map_err(|e| Crash::new(format!("cannot check out the focus: {}", e)))?;
2096− let clear = match on_disk {
781+ locked_ws
2097− None => Vec::new(),
782+ .finish(op_id)
2098− Some(on_disk) => match look_where_added(&root, markers, on_disk, &commit.tree()).await {
2099− Ok(Ok(clear)) => clear,
2100− Ok(Err(in_the_way)) => {
2101− return Err(Crash::new(format!("cannot check out the focus: {}", in_the_way_of(&in_the_way))));
2102− }
2103− Err(e) => return Err(Crash::new(format!("cannot check out the focus: {}", e))),
2104− },
2105− };
2106− // from here until the state is saved, a signal to stop waits
2107− let _deferral = Deferral::start();
2108− let published = op.is_some();
2109− let op_id = match op {
2110− None => self.current_repo().operation().id().clone(),
2111− Some(op) => {
2112− let repo = op
2113− .publish()
2114− .await
783+ .await
2115− .map_err(|e| Crash::new(format!("cannot publish the operation: {}", e)))?;
784+ .map_err(|e| Crash::new(format!("cannot finish the checkout: {}", e)))?;
2116− let op_id = repo.operation().id().clone();
2117− *self.inner.repo.lock().unwrap() = repo;
2118− op_id
2119− }
2120− };
2121− // what a crash from here on says: the state is left stale, which the
2122− // next run carries on from (§7.5 step 7)
2123− let carry_on = |what: &str| match back {
2124− None => String::new(),
2125− Some(back) if published => {
2126− format!("; the operation is recorded, but {}: the next command carries on from there{}", what, back)
2127− }
2128− Some(_) => format!("; {}: the next command carries on from there", what),
2129− };
2130− // A checkout cut short leaves the directory holding `on_disk` but
2131− // where it wrote, and the next run takes what it holds for changes
2132− // to the state (`snapshot_tree`). So the state records `on_disk`
2133− // first, at the operation it was saved at, which leaves it stale
2134− // until the checkout completes: were it left recording the tree
2135− // before the run's snapshot, the snapshot's changes, now in the
2136− // operation, would be taken for new ones and replayed onto the
2137− // focus too. It is saved only once `op` is published: a state
2138− // recording the snapshot, beside a head that does not, would lose
2139− // it (§7.4).
2140− if on_disk.is_some_and(|t| t.tree_ids() != locked_ws.locked_wc().old_tree().tree_ids()) {
2141− let old_op = locked_ws.locked_wc().old_operation_id().clone();
2142− let saved = match locked_ws.finish(old_op).await {
2143− Ok(()) => ws_guard.start_working_copy_mutation().await.map_err(|e| e.to_string()),
2144− Err(e) => Err(e.to_string()),
2145− };
2146− locked_ws = match saved {
2147− Ok(locked) => locked,
2148− Err(e) => {
2149− let what = "the working directory was not updated";
2150− return Err(Crash::new(format!("cannot save the working copy's state: {}{}", e, carry_on(what))));
2151− }
2152− };
2153− }
2154− // what is moved aside is put back where the checkout does not write,
2155− // or cuts the write short, once it returns, however it returns
2156− let mut aside = clear_the_way(&root, markers, &commit.tree(), &clear).await;
2157− let from = locked_ws.locked_wc().old_tree().clone();
2158− let what = "the working directory was only partly updated";
2159− let stats = match locked_ws.locked_wc().check_out(commit).await {
2160− Ok(stats) => stats,
2161− Err(e) => return Err(Crash::new(format!("cannot check out the focus: {}{}", e, carry_on(what)))),
2162− };
2163− // it cut no write short, so a start of a file moved aside, standing
2164− // at its path, was put there meanwhile
2165− aside.returned = true;
2166− // Something the directory does not track, put where the focus adds a
2167− // file since it was looked at, or a file holding what the checkout
2168− // writes there that could not be moved aside, made jj skip the
2169− // path, which it records as written. The state is left stale, as by
2170− // a checkout that fails, so that no run reads what stands there
2171− // (§7.5 step 7).
2172− if stats.skipped_files > 0 {
2173− let skipped = first_skipped(&root, markers, &from, &commit.tree()).await.ok().flatten();
2174− let why = match (skipped, &aside.stuck) {
2175− (Some(path), _) => format!(
2176− "`{}` was not written, as something untracked in the working directory was in its way (move it aside)",
2177− path.as_internal_file_string()
2178− ),
2179− (None, Some((path, e))) => format!(
2180− "`{}` was not written, as the untracked file there, holding what it writes, could not be moved out of its way: {}",
2181− path.as_internal_file_string(),
2182− e
2183− ),
2184− (None, None) => format!(
2185− "{} of its files were not written, as something untracked in the working directory was in their way",
2186− stats.skipped_files
2187− ),
2188− };
2189− return Err(Crash::new(format!("cannot check out the focus: {}{}", why, carry_on(what))));
2190− }
2191− // the state names the operation it was checked out at, so it is
2192− // saved only once that operation is published
2193− if let Err(e) = locked_ws.finish(op_id).await {
2194− let what = "the working copy's record of what the directory holds was not updated";
2195− return Err(Crash::new(format!("cannot save the working copy's state: {}{}", e, carry_on(what))));
2196− }
2197 Ok(())
785 Ok(())
2198 }
786 }
2199
787
2200 fn user_signature(&self, cfg: &Config) -> Result<Signature, Crash> {
788 fn user_signature(&self, cfg: &Config) -> Result<Signature, Crash> {
2201− let (name, email) = validated_identity(cfg)
789+ let (name, email) = crate::config::eval_user_only(&mut Interp::dummy(), cfg)?;
2202− .map_err(|(_, message)| Crash::new(message))?;
2203 Ok(Signature {
790 Ok(Signature {
2204 name,
791 name,
2205 email,
792 email,
@@ -2292,7 +879,6 @@ impl JjBackend {
2292 }
879 }
2293
880
2294 pub fn cmd_remote(&self, url: &str) -> Result<(), OpenError> {
881 pub fn cmd_remote(&self, url: &str) -> Result<(), OpenError> {
2295− let url = &origin_url(url)?;
2296 self.take_lock();
882 self.take_lock();
2297 let base = self.current_repo();
883 let base = self.current_repo();
2298 let origin = RemoteName::new("origin");
884 let origin = RemoteName::new("origin");
@@ -2310,17 +896,7 @@ impl JjBackend {
2310 .map_err(|e| (1, format!("cannot set the remote: {}", e)))
896 .map_err(|e| (1, format!("cannot set the remote: {}", e)))
2311 }
897 }
2312
898
2313− /// Run from the top of the workspace, as git runs from the top of its
2314− /// working tree: git resolves a relative local URL (one stored by git,
2315− /// or by `j` before §7.8 resolved them) against the directory it runs
2316− /// in, and jj-lib's git and ours inherit this process's
2317− fn enter_workspace_root(&self) -> Result<(), OpenError> {
2318− std::env::set_current_dir(&self.inner.workspace_root)
2319− .map_err(|e| (1, format!("cannot enter {}: {}", self.inner.workspace_root.display(), e)))
2320− }
2321−
2322 pub fn cmd_fetch(&self) -> Result<(), OpenError> {
899 pub fn cmd_fetch(&self) -> Result<(), OpenError> {
2323− self.enter_workspace_root()?;
2324 let origin = RemoteName::new("origin");
900 let origin = RemoteName::new("origin");
2325 let base = self.head_repo()?;
901 let base = self.head_repo()?;
2326 let mut tx = base.start_transaction();
902 let mut tx = base.start_transaction();
@@ -2333,52 +909,27 @@ impl JjBackend {
2333 let _ = block_on(self.publish_tx(tx, "fetch"))?;
909 let _ = block_on(self.publish_tx(tx, "fetch"))?;
2334 Ok(())
910 Ok(())
2335 }
911 }
2336− pub fn cmd_push(&self, cfg: &mut Config, expr_text: &str) -> Result<PushOutcome, OpenError> {
912+ pub fn cmd_push(&self, cfg: &Config, expr_text: &str) -> Result<(), OpenError> {
2337− self.enter_workspace_root()?;
2338 let origin = RemoteName::new("origin");
913 let origin = RemoteName::new("origin");
2339 let base = self.head_repo()?;
914 let base = self.head_repo()?;
2340− let (value, vis, immutable) = self.eval_push_expr(cfg, expr_text, &base)?;
915+ let (value, vis) = self.eval_push_expr(cfg, expr_text, &base)?;
2341 let records = parse_push_records(&value, &vis, &base)?;
916 let records = parse_push_records(&value, &vis, &base)?;
2342− if records.is_empty() {
2343− return Ok(PushOutcome::default());
2344− }
2345− let deletes_wait = check_push_records(&records, &base, &vis, &immutable)?;
917+ check_push_records(&records, &vis, &base)?;
2346− let mut outcome = PushOutcome::default();
918+ let _ = self.push_to_origin(&base, origin, &records, expr_text)?;
2347− for (name, target) in &records {
919+ Ok(())
2348− let before = base.view()
2349− .get_remote_bookmark(RefName::new(name).to_remote_symbol(origin))
2350− .target.as_resolved().cloned().unwrap_or(None);
2351− if &before == target {
2352− outcome.unchanged += 1;
2353− } else if target.is_none() {
2354− outcome.deleted += 1;
2355− } else {
2356− outcome.published += 1;
2357− }
2358− }
2359− let (_, clean) = self.inspect_working_copy(&base, false)?;
2360− if !clean {
2361− crate::terminal::report(format_args!(
2362− "j: warning: working copy has unrecorded changes; push sends recorded content only, so these edits will not be sent. Run `j id` to record them, then push again to include them."
2363− ));
2364− }
2365− let description = format!("push {}", expr_text);
2366− let _ = self.push_to_origin(&base, origin, &records, deletes_wait, &description)?;
2367− Ok(outcome)
2368 }
920 }
2369
921
2370 /// evaluate `EXPR` against the recorded repository, without snapshotting
922 /// evaluate `EXPR` against the recorded repository, without snapshotting
2371− /// (§1.1), apply a function result to the repo value once (§7.6); also
923+ /// (§1.1), apply a function result to the repo value once (§7.6)
2372− /// return the immutable set of that repository (§7.5 step 3)
2373 fn eval_push_expr(
924 fn eval_push_expr(
2374 &self,
925 &self,
2375− cfg: &mut Config,
926+ cfg: &Config,
2376 expr_text: &str,
927 expr_text: &str,
2377 base: &Arc<ReadonlyRepo>,
928 base: &Arc<ReadonlyRepo>,
2378− ) -> Result<(Value, Arc<VisibleRepo>, BTreeSet<String>), OpenError> {
929+ ) -> Result<(Value, Arc<VisibleRepo>), OpenError> {
2379 let outer = Rc::new(cfg.global_names.clone());
930 let outer = Rc::new(cfg.global_names.clone());
2380 let expr = crate::parse::parse_expr(expr_text, outer)
931 let expr = crate::parse::parse_expr(expr_text, outer)
2381− .map_err(|p| (3, crate::diagnostics::format_source_error("expression", expr_text, p.line, &p.msg)))?;
932+ .map_err(|p| (3, format!("line {}: {}", p.line, p.msg)))?;
2382 let (mut interp, _loaded, current) = self.build_interp(cfg, expr_text, false)?;
933 let (mut interp, _loaded, current) = self.build_interp(cfg, expr_text, false)?;
2383 let expr = crate::config::resolve_ids(&expr, &interp).map_err(|failures| {
934 let expr = crate::config::resolve_ids(&expr, &interp).map_err(|failures| {
2384 let (prefix, candidates) = &failures[0];
935 let (prefix, candidates) = &failures[0];
@@ -2389,30 +940,15 @@ impl JjBackend {
2389 }
940 }
2390 })?;
941 })?;
2391 crate::config::eval_config(&mut interp, cfg).map_err(|c| (3, format!("config.j: {}", c.msg)))?;
942 crate::config::eval_config(&mut interp, cfg).map_err(|c| (3, format!("config.j: {}", c.msg)))?;
2392− // the set a push may not move a bookmark out of (§7.6); build_interp
2393− // evaluated it for this value to place the focus, so it is cached
2394− let immutable = crate::repo::compute_immutable(&mut interp, &current)
2395− .map_err(|c| (1, format!("crash: {}", c.msg)))?;
2396− let expression_error = |crash: Crash| {
2397− let mut message = format!("crash: {}", crash.msg);
2398− match &crash.def {
2399− Some(name) => message.push_str(&format!("\n in {}, from push {}", name, expr_text)),
2400− None => message.push_str(&format!("\n from push {}", expr_text)),
2401− }
2402− if let Some(hint) = crate::diagnostics::push_expression_hint(expr_text, cfg, &crash) {
2403− message.push_str(&format!("\n {}", hint));
2404− }
2405− (1, message)
2406− };
2407 let env = interp.global_env();
943 let env = interp.global_env();
2408 let expr_rc = Rc::new(expr);
944 let expr_rc = Rc::new(expr);
2409 let mut v = interp
945 let mut v = interp
2410 .eval(&expr_rc, &env)
946 .eval(&expr_rc, &env)
2411− .map_err(&expression_error)?;
947+ .map_err(|c| (1, format!("crash: {}", c.msg)))?;
2412 if matches!(v, Value::Fun(_)) {
948 if matches!(v, Value::Fun(_)) {
2413 v = interp
949 v = interp
2414 .apply(v, current)
950 .apply(v, current)
2415− .map_err(expression_error)?;
951+ .map_err(|c| (1, format!("crash: {}", c.msg)))?;
2416 }
952 }
2417 let _ = base;
953 let _ = base;
2418 let vis = self
954 let vis = self
@@ -2422,28 +958,19 @@ impl JjBackend {
2422 .unwrap()
958 .unwrap()
2423 .clone()
959 .clone()
2424 .ok_or_else(|| (2, "internal: repository value not loaded".to_string()))?;
960 .ok_or_else(|| (2, "internal: repository value not loaded".to_string()))?;
2425− Ok((v, vis, immutable))
961+ Ok((v, vis))
2426 }
962 }
2427
963
2428 /// push the updates to `origin` and record the new remote-bookmark
964 /// push the updates to `origin` and record the new remote-bookmark
2429− /// positions as one operation (§7.6); when `deletes_wait`, the deletes
965+ /// positions as one operation (§7.6)
2430− /// go in a second git push, sent only once the remote has accepted every
2431− /// other update
2432 fn push_to_origin(
966 fn push_to_origin(
2433 &self,
967 &self,
2434 base: &Arc<ReadonlyRepo>,
968 base: &Arc<ReadonlyRepo>,
2435 origin: &RemoteName,
969 origin: &RemoteName,
2436 records: &[(String, Option<CommitId>)],
970 records: &[(String, Option<CommitId>)],
2437− deletes_wait: bool,
2438 description: &str,
971 description: &str,
2439 ) -> Result<Arc<ReadonlyRepo>, OpenError> {
972 ) -> Result<Arc<ReadonlyRepo>, OpenError> {
2440− // Never hand an empty refspec list to git, whose push.default could
2441− // select branches the expression did not request.
2442− if records.is_empty() {
2443− return Ok(base.clone());
2444− }
2445 let mut targets = jj_lib::git::GitPushRefTargets::default();
973 let mut targets = jj_lib::git::GitPushRefTargets::default();
2446− let mut deletes = jj_lib::git::GitPushRefTargets::default();
2447 for (name, after) in records {
974 for (name, after) in records {
2448 let before = base
975 let before = base
2449 .view()
976 .view()
@@ -2452,116 +979,48 @@ impl JjBackend {
2452 .as_resolved()
979 .as_resolved()
2453 .cloned()
980 .cloned()
2454 .unwrap_or(None);
981 .unwrap_or(None);
2455− // check_push_records sets `deletes_wait` only when some record
982+ targets
2456− // sets a bookmark, so the first push is never empty
2457− let batch = if deletes_wait && after.is_none() { &mut deletes } else { &mut targets };
2458− batch
2459 .bookmarks
983 .bookmarks
2460 .push((jj_lib::ref_name::RefNameBuf::from(name.clone()), Diff::new(before, after.clone())));
984 .push((jj_lib::ref_name::RefNameBuf::from(name.clone()), Diff::new(before, after.clone())));
2461 }
985 }
2462 let mut tx = base.start_transaction();
986 let mut tx = base.start_transaction();
2463 let subprocess_options = self.git_subprocess_options()?;
987 let subprocess_options = self.git_subprocess_options()?;
2464 let mut callback = QuietGitCallback;
988 let mut callback = QuietGitCallback;
2465− let mut push = |tx: &mut jj_lib::transaction::Transaction, targets| {
2466− jj_lib::git::push_refs(
989+ let stats = jj_lib::git::push_refs(
2467− tx.repo_mut(),
990+ tx.repo_mut(),
2468− subprocess_options.clone(),
991+ subprocess_options,
2469− origin,
992+ origin,
2470− targets,
993+ &targets,
2471− &mut callback,
994+ &mut callback,
2472− &jj_lib::git::GitPushOptions::default(),
995+ &jj_lib::git::GitPushOptions::default(),
2473− )
996+ )
2474− .map_err(|e| match e {
997+ .map_err(|e| match e {
2475− jj_lib::git::GitPushError::NoSuchRemote(_) => (1, "no remote origin".to_string()),
998+ jj_lib::git::GitPushError::NoSuchRemote(_) => (1, "no remote origin".to_string()),
2476− other => (1, format!("push failed: {}", other)),
999+ other => (1, format!("push failed: {}", other)),
2477− })
1000+ })?;
2478− };
2479− let mut stats = push(&mut tx, &targets)?;
2480− // git push is not atomic: the remote may accept some updates and
2481− // reject others. When only a bookmark this push sets keeps a deleted
2482− // bookmark's immutable commit reachable, the deletes go in a second
2483− // push once the remote has accepted every update, and are not sent
2484− // otherwise (§7.6)
2485− let mut not_sent: Vec<String> = Vec::new();
2486− let mut failed = None;
2487− if !deletes.bookmarks.is_empty() {
2488− if stats.all_ok() {
2489− match push(&mut tx, &deletes) {
2490− Ok(more) => {
2491− stats.pushed.extend(more.pushed);
2492− stats.rejected.extend(more.rejected);
2493− stats.remote_rejected.extend(more.remote_rejected);
2494− stats.unexported_bookmarks.extend(more.unexported_bookmarks);
2495− }
2496− Err(e) => failed = Some(e),
2497− }
2498− } else {
2499− for (name, _) in &deletes.bookmarks {
2500− not_sent.push(format!("refs/heads/{}", name.as_str()));
2501− }
2502− }
2503− }
2504− // push_refs has set the accepted updates in `tx`, and they are on the
2505− // remote, so they are recorded before the rest is reported; a push
2506− // the remote refused entirely records nothing
2507− let mut repo = base.clone();
2508− if stats.all_ok() || stats.some_exported() {
2509− let unpublished = block_on(tx.write(truncate_chars(description, 200)))
2510− .map_err(|e| (1, format!("cannot write the operation: {}", e)))?;
2511− repo = block_on(unpublished.publish())
2512− .map_err(|e| (1, format!("cannot publish the operation: {}", e)))?;
2513− *self.inner.repo.lock().unwrap() = repo.clone();
2514− }
2515− if let Some((code, msg)) = failed {
2516− return Err((code, format!("{}; the other bookmarks were pushed and recorded", msg)));
2517− }
2518 if !stats.all_ok() {
1001 if !stats.all_ok() {
2519 let mut names: Vec<String> = Vec::new();
1002 let mut names: Vec<String> = Vec::new();
2520 for (name, _) in stats.rejected.iter().chain(stats.remote_rejected.iter()) {
1003 for (name, _) in stats.rejected.iter().chain(stats.remote_rejected.iter()) {
2521 names.push(name.as_str().to_string());
1004 names.push(name.as_str().to_string());
2522 }
1005 }
2523− // pushed, but not exported to the colocated git repo, so jj-lib
2524− // left them out of `tx`
2525− for (symbol, _) in &stats.unexported_bookmarks {
2526− names.push(format!("{} (pushed but not recorded)", symbol.name.as_str()));
2527− }
2528− let mut msg = format!("push rejected: {}", names.join(", "));
2529− if !not_sent.is_empty() {
2530− msg.push_str(&format!("; deletes not sent: {}", not_sent.join(", ")));
2531− }
2532− if stats.some_exported() {
2533− msg.push_str("; the other bookmarks were pushed and recorded");
2534− }
2535− return Err((1, msg));
1006+ return Err((1, format!("push rejected: {}", names.join(", "))));
2536 }
1007 }
1008+ let unpublished = block_on(tx.write(truncate_chars(description, 200)))
1009+ .map_err(|e| (1, format!("cannot write the operation: {}", e)))?;
1010+ let repo = block_on(unpublished.publish())
1011+ .map_err(|e| (1, format!("cannot publish the operation: {}", e)))?;
1012+ *self.inner.repo.lock().unwrap() = repo.clone();
2537 Ok(repo)
1013 Ok(repo)
2538 }
1014 }
2539
1015
2540− pub fn cmd_undo(&self, redo: bool) -> Result<String, OpenError> {
1016+ pub fn cmd_undo(&self, redo: bool) -> Result<(), OpenError> {
2541 let base = self.head_repo()?;
1017 let base = self.head_repo()?;
2542− let on_disk = self.check_wc_clean(&base)?;
1018+ self.check_wc_clean(&base)?;
2543 let loader = self.repo_loader();
1019 let loader = self.repo_loader();
2544 let mut cur = base.operation().clone();
1020 let mut cur = base.operation().clone();
2545 let target_op_id: OperationId;
1021 let target_op_id: OperationId;
2546 loop {
1022 loop {
2547− let marker = op_marker(cur.metadata());
1023+ match (op_marker(cur.metadata()), redo) {
2548− if let Some(m) = marker.as_ref().filter(|m| m.restores_view) {
2549− // jj 0.45 names the operation whose view was restored.
2550− // Repeating the same direction follows that target; reversing
2551− // direction restores the marker's parent. The target may
2552− // itself be an undo or redo, not just a normal operation.
2553− if (m.kind == "redo") == redo {
2554− cur = block_on(loader.load_operation(&m.id))
2555− .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
2556− continue;
2557− }
2558− let Some(parent) = cur.parent_ids().first().cloned() else {
2559− return Err((1, if redo { "nothing to redo" } else { "nothing to undo" }.to_string()));
2560− };
2561− target_op_id = parent;
2562− break;
2563− }
2564− match (marker, redo) {
2565 (None, false) => {
1024 (None, false) => {
2566 let Some(parent) = cur.parent_ids().first().cloned() else {
1025 let Some(parent) = cur.parent_ids().first().cloned() else {
2567 return Err((1, "nothing to undo".to_string()));
1026 return Err((1, "nothing to undo".to_string()));
@@ -2587,16 +1046,7 @@ impl JjBackend {
2587 .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
1046 .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
2588 }
1047 }
2589 (Some(m), true) if m.kind == "undo" => {
1048 (Some(m), true) if m.kind == "undo" => {
2590− // An old j marker names the discarded normal operation,
2591− // but its parent has the same view and retains any earlier
2592− // undos. Modern redo markers must keep that navigation
2593− // point so another redo can continue across the upgrade.
2594− target_op_id = if m.authored_by_j {
1049+ target_op_id = m.id;
2595− cur.parent_ids().first().cloned()
2596− .ok_or_else(|| (1, "nothing to redo".to_string()))?
2597− } else {
2598− m.id
2599− };
2600 break;
1050 break;
2601 }
1051 }
2602 (Some(m), false) => {
1052 (Some(m), false) => {
@@ -2641,29 +1091,8 @@ impl JjBackend {
2641 }
1091 }
2642 }
1092 }
2643 }
1093 }
2644− // before the operation init or clone recorded there was no
2645− // repository (§7.7); the view jj recorded then has an empty child of
2646− // the root as the working-copy commit, and checking it out would
2647− // delete every file of the head
2648− if !redo && op_created_repo(cur.metadata()) {
2649− return Err((1, "nothing to undo".to_string()));
2650− }
2651− let mut target_op = block_on(loader.load_operation(&target_op_id))
1094+ let target_op = block_on(loader.load_operation(&target_op_id))
2652 .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
1095 .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
2653− // jj also follows markers in the selected direction at the target:
2654− // undoing a redo whose parent is an undo restores that undo's target,
2655− // and conversely for redo. Keep the same canonical navigation point.
2656− while let Some(m) = op_marker(target_op.metadata())
2657− .filter(|m| m.restores_view && (m.kind == "redo") == redo)
2658− {
2659− target_op = block_on(loader.load_operation(&m.id))
2660− .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
2661− }
2662− let target_op_id = target_op.id().clone();
2663− // Report the expression whose state is being reversed or restored,
2664− // following restore markers left by repeated undo/redo commands.
2665− let affected = if redo { &target_op } else { &cur };
2666− let affected_description = restored_operation_description(&loader, affected);
2667 let target_view = block_on(
1096 let target_view = block_on(
2668 loader
1097 loader
2669 .op_store()
1098 .op_store()
@@ -2687,94 +1116,65 @@ impl JjBackend {
2687 },
1116 },
2688 ));
1117 ));
2689 }
1118 }
2690− // Use jj's current marker so either CLI can continue this history.
2691− // The older j and jj marker formats remain readable above.
2692− let marker = format!(
1119+ let marker = if redo {
2693− "{}: restore to operation {}",
1120+ format!("redo: {}", cur.id().hex())
2694− if redo { "redo" } else { "undo" },
1121+ } else {
2695− target_op_id.hex()
1122+ format!("undo: {}", cur.id().hex())
2696− );
1123+ };
2697 let mut tx = base.start_transaction();
1124 let mut tx = base.start_transaction();
2698 tx.repo_mut().set_view(target_view.clone());
1125 tx.repo_mut().set_view(target_view.clone());
2699 let unpublished = block_on(tx.write(&marker))
1126 let unpublished = block_on(tx.write(&marker))
2700 .map_err(|e| (1, format!("cannot write the operation: {}", e)))?;
1127 .map_err(|e| (1, format!("cannot write the operation: {}", e)))?;
2701− // the operation is published, then the restored focus checked out
1128+ let op_id = unpublished.operation().id().clone();
2702− // from what the directory holds (§7.7)
1129+ let repo = block_on(unpublished.publish())
2703− let wc_id = &target_view.wc_commit_ids[WorkspaceName::DEFAULT];
2704− let wc_commit = block_on(base.store().get_commit_async(wc_id))
2705− .map_err(|e| (2, format!("cannot read the working-copy commit: {}", e)))?;
1130+ .map_err(|e| (1, format!("cannot publish the operation: {}", e)))?;
2706− let back = if redo { ", and `j undo` goes back" } else { ", and `j redo` goes back" };
1131+ *self.inner.repo.lock().unwrap() = repo.clone();
1132+ let wc_commit = block_on(self.wc_commit(&repo))?;
2707− block_on(self.checkout(&wc_commit, Some(&on_disk), Some(unpublished), Some(back)))
1133+ block_on(self.checkout(&wc_commit, op_id)).map_err(|c| (1, c.msg))?;
2708− .map_err(|c| (1, c.msg))?;
2709− let focus_message = wc_commit.description().lines().next().unwrap_or("").trim();
2710− Ok(format!(
1134+ Ok(())
2711− "{} `{}`; restored focus @{} {}",
2712− if redo { "Redid" } else { "Undid" },
2713− affected_description,
2714− wc_commit.change_id().reverse_hex(),
2715− terminal_text(if focus_message.is_empty() { "(no description)" } else { focus_message }),
2716− ))
2717− }
2718−
2719− /// Refuse, as `undo` and `redo` do before anything else (§7.7), when the
2720− /// working directory holds changes the working-copy commit does not:
2721− /// what a snapshot would record (§7.4), which their checkout would write
2722− /// over. Otherwise return the tree the directory holds, as scanned,
2723− /// which that checkout starts from: the commit's, but for the text
2724− /// written for a conflict the state does not record, as one lost and
2725− /// made again records none, or, where the working-copy state is stale,
2726− /// the state's with any changes to it the commit already has, such as
2727− /// what a checkout cut short wrote. Reserved commands never snapshot,
2728− /// so nothing is recorded either way.
2729− fn check_wc_clean(&self, base: &Arc<ReadonlyRepo>) -> Result<MergedTree, OpenError> {
2730− let (scanned, clean) = self.inspect_working_copy(base, true)?;
2731− if !clean {
2732− return Err((1, "working copy has changes not in @; run `j id` to record them or discard them. Recording creates a new operation: the next `j undo` undoes that recording, not the operation you originally wanted to undo; a new recording also clears pending redos. Use `j ops` to inspect the history".to_string()));
2733− }
2734− Ok(scanned)
2735 }
1135 }
2736
1136
2737− /// Compare a scan with the recorded focus without publishing a snapshot.
1137+ /// refuse if the working directory differs from the focused commit's
2738− /// Push only inspects: it must leave the saved working-copy state alone
1138+ /// files (§7.7): reserved commands never snapshot, so compare the locked
2739− /// so a later `j id` still sees every unrecorded edit.
1139+ /// working copy's recorded tree with the focus commit's tree
2740− fn inspect_working_copy(
2741− &self,
2742− base: &Arc<ReadonlyRepo>,
1140+ fn check_wc_clean(&self, base: &Arc<ReadonlyRepo>) -> Result<(), OpenError> {
2743− refresh_clean_state: bool,
1141+ let dirty = (1, "working copy has changes not in @; run `j id` to record them or discard them".to_string());
2744− ) -> Result<(MergedTree, bool), OpenError> {
2745− let wc_tree = block_on(self.wc_commit(base))?.tree();
1142+ let wc_commit = block_on(self.wc_commit(base))?;
1143+ // snapshot the working directory (without persisting anything) and
1144+ // compare against the focused commit's files (§7.7)
2746 let mut ws_guard = self.inner.workspace.lock().unwrap();
1145 let mut ws_guard = self.inner.workspace.lock().unwrap();
2747− let root = ws_guard.workspace_root().to_owned();
2748− let saved = SavedState::of(&ws_guard);
2749 let mut locked_ws = block_on(ws_guard.start_working_copy_mutation())
1146 let mut locked_ws = block_on(ws_guard.start_working_copy_mutation())
2750 .map_err(|e| (2, format!("cannot lock the working copy: {}", e)))?;
1147 .map_err(|e| (2, format!("cannot lock the working copy: {}", e)))?;
2751− let old_tree = locked_ws.locked_wc().old_tree().clone();
1148+ let options = SnapshotOptions {
1149+ base_ignores: GitIgnoreFile::empty(),
1150+ progress: None,
1151+ start_tracking_matcher: &EverythingMatcher,
1152+ force_tracking_matcher: &NothingMatcher,
1153+ max_new_file_size: u64::MAX,
1154+ };
2752− let scanned = block_on(scan(locked_ws.locked_wc(), &root, &old_tree, &saved, &snapshot_options()))
1155+ let (new_tree, _stats) = block_on(locked_ws.locked_wc().snapshot(&options))
2753 .map_err(|e| (2, format!("cannot read the working copy: {}", e)))?;
1156 .map_err(|e| (2, format!("cannot read the working copy: {}", e)))?;
1157+ let same = new_tree.tree_ids() == wc_commit.tree().tree_ids();
1158+ if same {
1159+ // the scanned tree is the one already recorded: saving the state
1160+ // only refreshes the mtime cache
2754− let current = locked_ws.locked_wc().old_operation_id() == base.operation().id();
1161+ block_on(locked_ws.finish(base.operation().id().clone()))
2755− let recorded = match marker_style(base.settings()) {
2756− Ok(markers) => block_on(snapshot_tree(&root, markers, &wc_tree, &old_tree, current, &scanned)),
2757− Err(e) => Err(e),
2758− }
2759− .map_err(|e| (2, format!("cannot read the working copy: {}", e)))?;
1162+ .map_err(|e| (2, format!("cannot finish: {}", e)))?;
2760− if recorded.tree_ids() != wc_tree.tree_ids() {
1163+ } else {
1164+ // the working copy differs from @ and this command records
2761− // drop the lock without saving, or the state would claim the
1165+ // nothing: drop the lock without saving, or the state would claim
2762− // directory was snapshotted and hide the changes from the next
1166+ // the directory was snapshotted and hide the changes from the next
2763 // run (§7.7)
1167 // run (§7.7)
2764− return Ok((scanned, false));
1168+ drop(locked_ws);
2765 }
1169 }
2766− // saving the state only refreshes what it knows of each file, or
1170+ drop(ws_guard);
2767− // takes in what the commit already has
1171+ if !same {
2768− if refresh_clean_state {
1172+ return Err(dirty);
2769− block_on(save_scan(locked_ws, &scanned, &wc_tree, base.operation().id()))
2770− .map_err(|e| (2, format!("cannot finish: {}", e)))?;
2771 }
1173 }
2772− Ok((scanned, true))
1174+ Ok(())
2773 }
1175 }
2774
1176
2775− /// The operation log for `ops` (§7.7), one line per operation, newest
2776− /// first; the caller writes it to stdout.
2777− pub fn cmd_ops(&self) -> Result<String, OpenError> {
1177+ pub fn cmd_ops(&self) -> Result<(), OpenError> {
2778 let base = self.head_repo()?;
1178 let base = self.head_repo()?;
2779 let loader = self.repo_loader();
1179 let loader = self.repo_loader();
2780 let head = base.operation().clone();
1180 let head = base.operation().clone();
@@ -2787,8 +1187,8 @@ impl JjBackend {
2787 let mut tag = "";
1187 let mut tag = "";
2788 match op_kind(op.metadata()) {
1188 match op_kind(op.metadata()) {
2789 OpKind::Normal => {}
1189 OpKind::Normal => {}
2790− OpKind::Undo => tag = "undo",
1190+ OpKind::Undo { .. } => tag = "undo",
2791− OpKind::Redo => tag = "redo",
1191+ OpKind::Redo { .. } => tag = "redo",
2792 }
1192 }
2793 if desc.is_empty() {
1193 if desc.is_empty() {
2794 desc = "(no description)".to_string();
1194 desc = "(no description)".to_string();
@@ -2811,7 +1211,8 @@ impl JjBackend {
2811 None => None,
1211 None => None,
2812 };
1212 };
2813 }
1213 }
2814− Ok(out)
1214+ print!("{}", out);
1215+ Ok(())
2815 }
1216 }
2816}
1217}
2817
1218
@@ -2848,7 +1249,6 @@ pub fn cmd_init(cfg: &Config) -> Result<(), OpenError> {
2848 }
1249 }
2849 .map_err(|e| (2, format!("cannot create the repository: {}", e)))?;
1250 .map_err(|e| (2, format!("cannot create the repository: {}", e)))?;
2850 let backend = JjBackend {
1251 let backend = JjBackend {
2851− replay_cache: Rc::new(ReplayCache::default()),
2852 inner: Arc::new(JjInner {
1252 inner: Arc::new(JjInner {
2853 workspace_root: cwd.clone(),
1253 workspace_root: cwd.clone(),
2854 workspace: Mutex::new(workspace),
1254 workspace: Mutex::new(workspace),
@@ -2857,43 +1257,23 @@ pub fn cmd_init(cfg: &Config) -> Result<(), OpenError> {
2857 visible: Mutex::new(None),
1257 visible: Mutex::new(None),
2858 lock_guard: Mutex::new(None),
1258 lock_guard: Mutex::new(None),
2859 pending: Mutex::new(None),
1259 pending: Mutex::new(None),
2860− folds_case: OnceLock::new(),
2861− long_names: Mutex::new(HashMap::new()),
2862 }),
1260 }),
2863 };
1261 };
2864− // the import and the working-copy commit are one operation, the one
2865− // undo never undoes (§7.7)
2866− let mut tx = backend.current_repo().start_transaction();
2867 if had_git {
1262 if had_git {
2868 let import_options = backend.git_import_options()?;
1263 let import_options = backend.git_import_options()?;
1264+ let mut tx = backend.current_repo().start_transaction();
2869 block_on(jj_lib::git::import_refs(tx.repo_mut(), &import_options))
1265 block_on(jj_lib::git::import_refs(tx.repo_mut(), &import_options))
2870 .map_err(|e| (2, format!("cannot import the git history: {}", e)))?;
1266 .map_err(|e| (2, format!("cannot import the git history: {}", e)))?;
2871− // HEAD too: it is the head the working-copy commit starts on, and a
2872− // detached HEAD's commits are reachable from no ref
2873− block_on(jj_lib::git::import_head(
2874− tx.repo_mut(),
2875− &backend.inner.workspace_name,
2876− &cwd,
2877− ))
2878− .map_err(|e| (2, format!("cannot import the git HEAD: {}", e)))?;
2879 block_on(tx.repo_mut().rebase_descendants())
1267 block_on(tx.repo_mut().rebase_descendants())
2880 .map_err(|e| (2, format!("cannot rebase descendants: {}", e)))?;
1268 .map_err(|e| (2, format!("cannot rebase descendants: {}", e)))?;
1269+ let _ = block_on(backend.publish_tx(tx, "import git refs"))?;
2881 }
1270 }
2882− // §7.8: the current head is git's HEAD (none on an unborn branch)
2883− let head = tx
2884− .repo()
2885− .view()
2886− .git_head(&backend.inner.workspace_name)
2887− .as_resolved()
2888− .cloned()
2889− .flatten();
2890− create_initial_wc_commit(&backend, cfg, tx, "init", head, had_git, Some(""))?;
1271+ create_initial_wc_commit(&backend, cfg, "init")?;
2891 Ok(())
1272 Ok(())
2892}
1273}
2893
1274
2894pub fn cmd_clone(cfg: &Config, url: &str, dir: &str) -> Result<(), OpenError> {
1275pub fn cmd_clone(cfg: &Config, url: &str, dir: &str) -> Result<(), OpenError> {
2895 let settings = user_settings_strict(cfg)?;
1276 let settings = user_settings_strict(cfg)?;
2896− let url = &origin_url(url)?;
2897 let dir_path = PathBuf::from(dir);
1277 let dir_path = PathBuf::from(dir);
2898 if dir_path.join(".jj").is_dir() {
1278 if dir_path.join(".jj").is_dir() {
2899 return Err((2, format!("{} already contains a jj repository", dir)));
1279 return Err((2, format!("{} already contains a jj repository", dir)));
@@ -2907,296 +1287,82 @@ pub fn cmd_clone(cfg: &Config, url: &str, dir: &str) -> Result<(), OpenError> {
2907 } else if dir_path.exists() {
1287 } else if dir_path.exists() {
2908 return Err((2, format!("{} already exists and is not empty", dir)));
1288 return Err((2, format!("{} already exists and is not empty", dir)));
2909 }
1289 }
2910− let existed = dir_path.is_dir();
1290+ std::fs::create_dir_all(&dir_path)
2911− // the directory an existing DIR leads to, through a symlink DIR may be:
2912− // a clone that fails empties it only while DIR still leads there (§7.8)
2913− let found = if existed { dir_identity(&dir_path, true) } else { None };
2914− // the directories this run creates, in order, each with its device and
2915− // inode: a clone that fails removes DIR and those of its parents left
2916− // empty, or an existing DIR's entries, and nothing else (§7.8).
2917− // Each is made by `create_dir`, whose success proves it new. The checks
2918− // above read DIR as written, before its missing parents exist; once
2919− // they do, `new/..` or `new/../keep` names a directory that was there,
2920− // so DIR's own `create_dir` failing, AlreadyExists included, refuses it
2921− // as `git clone` does
2922− let mut created: Vec<(&std::path::Path, Option<(u64, u64)>)> = Vec::new();
2923− // those of them a failed clone leaves, as their paths no longer lead
2924− // to them
2925− let mut left: Vec<&std::path::Path> = Vec::new();
2926− // from here until the clone has removed what it made or completed, a
2927− // signal to stop waits, and fails a clone that has not yet made its
2928− // working-copy commit (`unless_interrupted`, §1.3)
2929− let _held = Deferral::start();
2930− if !existed {
2931− // DIR and its missing parents, DIR first ("" is the current
2932− // directory, which exists)
2933− let missing: Vec<&std::path::Path> = dir_path
2934− .ancestors()
2935− .enumerate()
2936− .take_while(|(i, p)| *i == 0 || !(p.as_os_str().is_empty() || p.exists()))
2937− .map(|(_, p)| p)
2938− .collect();
2939− for p in missing.into_iter().rev() {
2940− match std::fs::create_dir(p) {
2941− Ok(()) => created.push((p, dir_identity(p, false))),
2942− // a parent reached again through `..`
2943− Err(e)
2944− if e.kind() == std::io::ErrorKind::AlreadyExists
2945− && p != dir_path
2946− && p.is_dir() => {}
2947− Err(e) => {
2948− for &(p, id) in created.iter().rev() {
2949− if !remove_created_dir(p, id, false) {
2950− left.push(p);
2951− }
2952− }
2953− let msg = format!("cannot create {}: {}", dir, e);
1291+ .map_err(|e| (2, format!("cannot create {}: {}", dir, e)))?;
2954− return Err((2, msg + &left_note(&left)));
2955− }
2956− }
2957− }
2958− }
2959− let cloned = clone_into(cfg, &settings, url, &dir_path);
2960− let Err((code, msg)) = cloned else {
2961− return Ok(());
2962− };
2963− // like `git clone`, a clone that fails leaves nothing behind: its
2964− // working-copy commit would claim files the checkout never wrote, and a
2965− // later clone would refuse the directory (§7.8)
2966− if !existed {
2967− // DIR, the last one created, goes whole; a parent holds nothing of
2968− // the clone's but DIR, so it goes only if left empty: what else was
2969− // put there meanwhile, a sibling clone say, stays
2970− for (i, &(p, id)) in created.iter().enumerate().rev() {
2971− if !remove_created_dir(p, id, i + 1 == created.len()) {
2972− left.push(p);
2973− }
2974− }
2975− Err((code, msg + &left_note(&left)))
2976− } else if found.is_some() && dir_identity(&dir_path, true) == found {
2977− if let Ok(entries) = std::fs::read_dir(&dir_path) {
2978− for entry in entries.flatten() {
2979− let _ = match entry.file_type() {
2980− Ok(t) if t.is_dir() => std::fs::remove_dir_all(entry.path()),
2981− _ => std::fs::remove_file(entry.path()),
2982− };
2983− }
2984− }
2985− Err((code, msg))
2986− } else if dir_path.symlink_metadata().is_ok() {
2987− // a symlink put in place of DIR, or of a directory above it, would
2988− // have `read_dir` list what that names
2989− Err((
2990− code,
2991− format!(
2992− "{}; left what `{}` holds, as it is no longer the directory this clone found empty",
2993− msg, dir
2994− ),
2995− ))
2996− } else {
2997− Err((code, msg))
2998− }
2999−}
3000−
3001−/// The device and inode of the directory at `path`, following a symlink
3002−/// there only when `follow` says so; `None` when `path` holds no directory
3003−fn dir_identity(path: &std::path::Path, follow: bool) -> Option<(u64, u64)> {
3004− use std::os::unix::fs::MetadataExt;
3005− let meta = if follow {
3006− std::fs::metadata(path)
3007− } else {
3008− std::fs::symlink_metadata(path)
3009− };
3010− meta.ok().filter(|m| m.is_dir()).map(|m| (m.dev(), m.ino()))
3011−}
3012−
3013−/// Remove the directory a failed clone created at `path`, `whole` or only
3014−/// if empty, while `path` still leads to it: the directory whose device and
3015−/// inode `id` were read on creating it (§7.8). A symlink put in its place,
3016−/// or in place of a directory above it, would have the removal follow it
3017−/// and delete what that names, so a path leading anywhere else is left, and
3018−/// the result is false. One leading nowhere holds nothing to remove.
3019−fn remove_created_dir(path: &std::path::Path, id: Option<(u64, u64)>, whole: bool) -> bool {
3020− if id.is_some() && dir_identity(path, false) == id {
3021− let _ = if whole {
3022− std::fs::remove_dir_all(path)
3023− } else {
3024− std::fs::remove_dir(path)
3025− };
3026− true
3027− } else {
3028− path.symlink_metadata().is_err()
3029− }
3030−}
3031−
3032−/// What a failed clone's error adds for the directories it created and
3033−/// `left` (§7.8): nothing when it left none
3034−fn left_note(left: &[&std::path::Path]) -> String {
3035− let named = |ps: &[&std::path::Path]| {
3036− ps.iter().map(|p| format!("`{}`", p.display())).collect::<Vec<_>>().join(", ")
3037− };
3038− match left {
3039− [] => String::new(),
3040− [one] => format!(
3041− "; left {}, as it is no longer the directory this clone created",
3042− named(&[one])
3043− ),
3044− [init @ .., last] => format!(
3045− "; left {} and {}, as they are no longer the directories this clone created",
3046− named(init),
3047− named(&[last])
3048− ),
3049− }
3050−}
3051−
3052−/// `url` as `origin` holds it (§7.8): a local path resolved against the
3053−/// current directory, as `jj git clone` stores it, for git resolves a
3054−/// relative one against the directory each later `fetch` or `push` runs
3055−/// in; one under a home directory (`~/…`, `~user/…`) and any other URL, a
3056−/// `file://` one included, as given. One that does not parse, or a path
3057−/// that does not resolve to UTF-8, is a usage error.
3058−fn origin_url(url: &str) -> Result<String, OpenError> {
3059− let mut parsed = gix::url::parse(url).map_err(|e| (2, format!("invalid URL `{}`: {}", url, e)))?;
3060− // a bare path is a file location in the alternative form
3061− if parsed.scheme != gix::url::Scheme::File || !parsed.serialize_alternative_form {
3062− return Ok(url.to_string());
3063− }
3064− // git expands a leading `~` to the home directory on every fetch and
3065− // push, from wherever it runs; resolved here, it would name a directory
3066− // called `~` (`./~/…` still does)
3067− if parsed.path.starts_with(b"~") {
3068− return Ok(url.to_string());
3069− }
3070− let cwd = std::env::current_dir()
3071− .map_err(|e| (2, format!("cannot read the current directory: {}", e)))?;
3072− parsed
3073− .canonicalize(&cwd)
3074− .map_err(|e| (2, format!("cannot resolve `{}`: {}", url, e)))?;
3075− // `add_remote` takes UTF-8, which only the current directory's name or
3076− // a symlink's target can break; stored as given, the path would reach
3077− // the remote only from where it was typed
3078− String::from_utf8(parsed.to_bstring().into())
3079− .map_err(|_| (2, format!("cannot resolve `{}`: the path it names is not UTF-8", url)))
3080−}
3081−
3082−/// `clone`'s work once `dir_path` exists and is empty (§7.8)
3083−fn clone_into(
3084− cfg: &Config,
3085− settings: &UserSettings,
3086− url: &str,
3087− dir_path: &std::path::Path,
3088−) -> Result<(), OpenError> {
3089 let (workspace, repo) = block_on(Workspace::init_colocated_git(
1292 let (workspace, repo) = block_on(Workspace::init_colocated_git(
3090− settings,
1293+ &settings,
3091− dir_path,
1294+ &dir_path,
3092 gix::hash::Kind::Sha1,
1295 gix::hash::Kind::Sha1,
3093 ))
1296 ))
3094 .map_err(|e| (2, format!("cannot create the repository: {}", e)))?;
1297 .map_err(|e| (2, format!("cannot create the repository: {}", e)))?;
3095 let backend = JjBackend {
1298 let backend = JjBackend {
3096− replay_cache: Rc::new(ReplayCache::default()),
3097 inner: Arc::new(JjInner {
1299 inner: Arc::new(JjInner {
3098− workspace_root: dir_path.to_path_buf(),
1300+ workspace_root: dir_path.clone(),
3099 workspace: Mutex::new(workspace),
1301 workspace: Mutex::new(workspace),
3100 repo: Mutex::new(repo),
1302 repo: Mutex::new(repo),
3101 workspace_name: WorkspaceName::DEFAULT.to_owned(),
1303 workspace_name: WorkspaceName::DEFAULT.to_owned(),
3102 visible: Mutex::new(None),
1304 visible: Mutex::new(None),
3103 lock_guard: Mutex::new(None),
1305 lock_guard: Mutex::new(None),
3104 pending: Mutex::new(None),
1306 pending: Mutex::new(None),
3105− folds_case: OnceLock::new(),
3106− long_names: Mutex::new(HashMap::new()),
3107 }),
1307 }),
3108 };
1308 };
3109 let origin = RemoteName::new("origin");
1309 let origin = RemoteName::new("origin");
3110 let mut tx = backend.current_repo().start_transaction();
1310 let mut tx = backend.current_repo().start_transaction();
3111− // a signal to stop fails each step before the working-copy commit's
3112− // (§1.3)
3113− unless_interrupted(
3114− jj_lib::git::add_remote(tx.repo_mut(), origin, url, None)
1311+ jj_lib::git::add_remote(tx.repo_mut(), origin, url, None)
3115− .map_err(|e| (1, format!("cannot set the remote: {}", e))),
1312+ .map_err(|e| (1, format!("cannot set the remote: {}", e)))?;
3116− )?;
3117− unless_interrupted(backend.git_fetch_refs(tx.repo_mut(), origin))?;
1313+ backend.git_fetch_refs(tx.repo_mut(), origin)?;
3118− let default_branch = unless_interrupted(git_default_branch(
3119− git_backend(tx.repo().store())?.git_repo_path(),
3120− origin,
3121− ))?;
3122 let import_options = backend.git_import_options()?;
1314 let import_options = backend.git_import_options()?;
3123− unless_interrupted(
3124− block_on(jj_lib::git::import_refs(tx.repo_mut(), &import_options))
1315+ block_on(jj_lib::git::import_refs(tx.repo_mut(), &import_options))
3125− .map_err(|e| (1, format!("cannot import the fetched refs: {}", e))),
1316+ .map_err(|e| (1, format!("cannot import the fetched refs: {}", e)))?;
3126− )?;
3127− unless_interrupted(
3128− block_on(tx.repo_mut().rebase_descendants())
1317+ block_on(tx.repo_mut().rebase_descendants())
3129− .map_err(|e| (1, format!("cannot rebase descendants: {}", e))),
1318+ .map_err(|e| (1, format!("cannot rebase descendants: {}", e)))?;
3130− )?;
1319+ let _ = block_on(backend.publish_tx(tx, &format!("clone {}", url)))?;
3131− // §7.8: the default bookmark's target (none when the remote's HEAD names
3132− // no branch, or a branch it does not have)
3133− let head = default_branch.and_then(|name| {
3134− tx.repo()
3135− .view()
3136− .get_remote_bookmark(RefName::new(&name).to_remote_symbol(origin))
3137− .target
3138− .as_resolved()
3139− .cloned()
3140− .flatten()
3141− });
3142− // the fetch and the working-copy commit are one operation, the one undo
3143− // never undoes (§7.7)
3144− create_initial_wc_commit(&backend, cfg, tx, &format!("clone {}", url), head, false, None)?;
1320+ create_initial_wc_commit(&backend, cfg, &format!("clone {}", url))?;
3145 Ok(())
1321 Ok(())
3146}
1322}
3147
1323
3148−/// §7.8: after init/clone, create a working-copy commit as a child of
1324+/// §7.8: after init/clone, create a working-copy commit as a child of the
3149−/// `head` (the root commit when there is none), record it in `tx` with
1325+/// current head (a bookmark target if there is one, otherwise the root
3150−/// what the command has imported, and check it out. The commit holds its
1326+/// commit) and check it out.
3151−/// parent's files less any a checkout cannot create (`checkout_tree`), so
3152−/// it is empty unless the parent has one. `adopt` says the working
3153−/// directory already holds a checkout of the parent (init over an existing
3154−/// git repository): the working copy is then reset to the commit instead of
3155−/// written, so no file is touched and what the directory holds beyond the
3156−/// parent is the next snapshot's change. `back` is what a checkout that
3157−/// fails says (`JjBackend::checkout`): none for a clone, which then removes
3158−/// what it made.
3159fn create_initial_wc_commit(
1327fn create_initial_wc_commit(
3160 backend: &JjBackend,
1328 backend: &JjBackend,
3161 cfg: &Config,
1329 cfg: &Config,
3162− mut tx: jj_lib::transaction::Transaction,
3163 description: &str,
1330 description: &str,
3164− head: Option<CommitId>,
3165− adopt: bool,
3166− back: Option<&str>,
3167) -> Result<(), OpenError> {
1331) -> Result<(), OpenError> {
3168− let store = tx.base_repo().store().clone();
1332+ let base = backend.current_repo();
1333+ let store = base.store().clone();
1334+ let mut parent = store.root_commit_id().clone();
1335+ 'outer: for (_name, target) in base.view().local_bookmarks() {
1336+ if let Some(id) = target.as_resolved().and_then(|t| t.clone()) {
1337+ parent = id;
1338+ break 'outer;
1339+ }
1340+ }
3169− let parent = head.unwrap_or_else(|| store.root_commit_id().clone());
1341+ if parent == *store.root_commit_id() {
1342+ for (_name, remote_ref) in base.view().remote_bookmarks(RemoteName::new("origin")) {
1343+ if let Some(id) = remote_ref.target.as_resolved().and_then(|t| t.clone()) {
1344+ parent = id;
1345+ break;
1346+ }
1347+ }
1348+ }
3170 // jj's own init already created a working-copy commit; if it is already
1349 // jj's own init already created a working-copy commit; if it is already
3171 // a child of the target parent, keep it and just check it out
1350 // a child of the target parent, keep it and just check it out
3172 let mut to_abandon: Option<CommitId> = None;
1351 let mut to_abandon: Option<CommitId> = None;
3173− let existing = tx.repo().view().get_wc_commit_id(&backend.inner.workspace_name).cloned();
1352+ if let Ok(existing) = block_on(backend.wc_commit(&base)) {
3174− if let Some(existing) = existing.and_then(|id| block_on(store.get_commit_async(&id)).ok()) {
3175 if existing.parent_ids().first() == Some(&parent) {
1353 if existing.parent_ids().first() == Some(&parent) {
3176− // jj's commit is an empty child of the root; record only what
1354+ let op_id = base.operation().id().clone();
3177− // was imported, if anything
3178− let op = if tx.repo().has_changes() {
3179− Some(
3180− block_on(tx.write(truncate_chars(description, 200)))
3181− .map_err(|e| (1, format!("cannot write the operation: {}", e)))?,
3182− )
3183− } else {
3184− None
3185− };
3186− block_on(backend.checkout(&existing, None, op, back)).map_err(|c| (1, c.msg))?;
1355+ block_on(backend.checkout(&existing, op_id)).map_err(|c| (1, c.msg))?;
3187 return Ok(());
1356 return Ok(());
3188 }
1357 }
3189 if existing.id() != store.root_commit_id() {
1358 if existing.id() != store.root_commit_id() {
3190 to_abandon = Some(existing.id().clone());
1359 to_abandon = Some(existing.id().clone());
3191 }
1360 }
3192 }
1361 }
3193− let parent_tree = block_on(store.get_commit_async(&parent))
3194− .map_err(|e| (1, format!("cannot read the parent commit: {}", e)))?
3195− .tree();
3196− let wc_tree = block_on(checkout_tree(parent_tree, backend))?;
3197 let user_sig = backend
1362 let user_sig = backend
3198 .user_signature(cfg)
1363 .user_signature(cfg)
3199 .map_err(|c| (3, format!("config.j: {}", c.msg)))?;
1364 .map_err(|c| (3, format!("config.j: {}", c.msg)))?;
1365+ let mut tx = base.start_transaction();
3200 if let Some(old_wc) = &to_abandon {
1366 if let Some(old_wc) = &to_abandon {
3201 tx.repo_mut().record_abandoned_commit_with_parents(
1367 tx.repo_mut().record_abandoned_commit_with_parents(
3202 old_wc.clone(),
1368 old_wc.clone(),
@@ -3205,7 +1371,7 @@ fn create_initial_wc_commit(
3205 }
1371 }
3206 let wc = block_on(
1372 let wc = block_on(
3207 tx.repo_mut()
1373 tx.repo_mut()
3208− .new_commit(vec![parent], wc_tree)
1374+ .new_commit(vec![parent], store.empty_merged_tree())
3209 .set_author(user_sig.clone())
1375 .set_author(user_sig.clone())
3210 .set_committer(user_sig)
1376 .set_committer(user_sig)
3211 .write(),
1377 .write(),
@@ -3218,49 +1384,19 @@ fn create_initial_wc_commit(
3218 .map_err(|e| (1, format!("cannot rebase descendants: {}", e)))?;
1384 .map_err(|e| (1, format!("cannot rebase descendants: {}", e)))?;
3219 let unpublished = block_on(tx.write(truncate_chars(description, 200)))
1385 let unpublished = block_on(tx.write(truncate_chars(description, 200)))
3220 .map_err(|e| (1, format!("cannot write the operation: {}", e)))?;
1386 .map_err(|e| (1, format!("cannot write the operation: {}", e)))?;
3221− // an adopted directory is reset to the commit, not checked out over:
1387+ let op_id = unpublished.operation().id().clone();
3222− // checking out from the empty tree jj's init recorded would recreate
1388+ let repo = block_on(unpublished.publish())
3223− // the files the user deleted there. Anything else is written, once the
1389+ .map_err(|e| (1, format!("cannot publish the operation: {}", e)))?;
3224− // operation is published (§7.5 step 7).
1390+ *backend.inner.repo.lock().unwrap() = repo;
3225− let on_disk = if adopt { Some(wc.tree()) } else { None };
3226− block_on(backend.checkout(&wc, on_disk.as_ref(), Some(unpublished), back)).map_err(|c| (1, c.msg))?;
1391+ block_on(backend.checkout(&wc, op_id)).map_err(|c| (1, c.msg))?;
3227 Ok(())
1392 Ok(())
3228}
1393}
3229
1394
3230−/// `tree` without the files at a path a checkout cannot create (§7.5 step
3231−/// 1), which a git branch can hold: a `.jj` directory committed by mistake,
3232−/// a name the workspace's filesystem does not hold. jj's checkout stops at
3233−/// the first such path, failing the whole clone, so the working-copy commit
3234−/// init or clone starts on leaves them out, and their removal is its change
3235−/// (§7.8).
3236−async fn checkout_tree(tree: MergedTree, backend: &JjBackend) -> Result<MergedTree, OpenError> {
3237− let folds_case = backend.folds_case();
3238− let refused: Vec<RepoPathBuf> = tree
3239− .entries()
3240− .map(|(path, _)| path)
3241− .filter(|path| {
3242− path.components().any(|c| {
3243− crate::repo::checkout_refuses(c.as_internal_str(), folds_case, |name| backend.name_fits(name))
3244− })
3245− })
3246− .collect();
3247− if refused.is_empty() {
3248− return Ok(tree);
3249− }
3250− let mut builder = MergedTreeBuilder::new(tree);
3251− for path in refused {
3252− builder.set_or_remove(path, Merge::absent());
3253− }
3254− builder
3255− .write_tree()
3256− .await
3257− .map_err(|e| (1, format!("cannot write a tree: {}", e)))
3258−}
3259−
3260/// user settings for init/clone: a missing or malformed `user` is a
1395/// user settings for init/clone: a missing or malformed `user` is a
3261/// configuration error (exit 3) (§7.9)
1396/// configuration error (exit 3) (§7.9)
3262fn user_settings_strict(cfg: &Config) -> Result<UserSettings, OpenError> {
1397fn user_settings_strict(cfg: &Config) -> Result<UserSettings, OpenError> {
3263− let (name, email) = validated_identity(cfg)?;
1398+ let (name, email) = crate::config::eval_user_only(&mut Interp::dummy(), cfg)
1399+ .map_err(|c| (3, format!("config.j: {}", c.msg)))?;
3264 let mut config = StackedConfig::with_defaults();
1400 let mut config = StackedConfig::with_defaults();
3265 let text = format!(
1401 let text = format!(
3266 "[user]\nname = {}\nemail = {}\n",
1402 "[user]\nname = {}\nemail = {}\n",
@@ -3274,39 +1410,6 @@ fn user_settings_strict(cfg: &Config) -> Result<UserSettings, OpenError> {
3274 .map_err(|e| (3, format!("config.j: invalid user settings: {}", e)))
1410 .map_err(|e| (3, format!("config.j: invalid user settings: {}", e)))
3275}
1411}
3276
1412
3277−/// The identity shown during repository setup. Validation is deliberately
3278−/// confined to authoring boundaries so inspection and recovery still work
3279−/// with an unchanged reference configuration.
3280−pub fn configured_identity(cfg: &Config) -> Result<String, OpenError> {
3281− let (name, email) = validated_identity(cfg)?;
3282− Ok(format!("{} <{}>", terminal_text(&name), terminal_text(&email)))
3283−}
3284−
3285−fn terminal_text(text: &str) -> String {
3286− text.chars().map(|c| {
3287− if c.is_control() { c.escape_debug().to_string() } else { c.to_string() }
3288− }).collect()
3289−}
3290−
3291−fn validated_identity(cfg: &Config) -> Result<(String, String), OpenError> {
3292− let (name, email) = crate::config::eval_user_only(&mut Interp::dummy(), cfg)
3293− .map_err(|c| (3, format!("config.j: {}", c.msg)))?;
3294− check_identity(&name, &email)?;
3295− Ok((name, email))
3296−}
3297−
3298−fn check_identity(name: &str, email: &str) -> Result<(), OpenError> {
3299− if name.trim().is_empty() || email.trim().is_empty()
3300− || name.trim() == "Your Name" || email.trim().eq_ignore_ascii_case("you@example.com")
3301− {
3302− return Err((3, format!(
3303− "config.j: user identity is not configured ({} <{}>); set `user.name` and `user.email` in the `user` record before creating or recording commits",
3304− terminal_text(name), terminal_text(email),
3305− )));
3306− }
3307− Ok(())
3308−}
3309−
3310// ----------------------------------------------------------------------
1413// ----------------------------------------------------------------------
3311// reserved-command helpers
1414// reserved-command helpers
3312// ----------------------------------------------------------------------
1415// ----------------------------------------------------------------------
@@ -3370,72 +1473,41 @@ fn git_fetch_subprocess(git_dir: &std::path::Path, remote: &RemoteName) -> Resul
3370 if out.status.success() {
1473 if out.status.success() {
3371 Ok(())
1474 Ok(())
3372 } else {
1475 } else {
1476+ Err((
1477+ 1,
1478+ format!(
3373− Err((1, format!("fetch failed: {}", git_failure(&out))))
1479+ "fetch failed: {}",
3374− }
3375−}
3376−
3377−/// What a git that failed said, or how it ended when it said nothing (a
3378−/// Ctrl-C stops it silently)
3379−fn git_failure(out: &std::process::Output) -> String {
3380− use std::os::unix::process::ExitStatusExt;
3381− let said = String::from_utf8_lossy(&out.stderr).trim().to_string();
1480+ String::from_utf8_lossy(&out.stderr).trim()
3382− match (said.is_empty(), out.status.signal()) {
1481+ ),
3383− (false, _) => said,
1482+ ))
3384− (true, Some(sig)) => format!("git was stopped by signal {}", sig),
3385− (true, None) => format!("git exited with status {}", out.status.code().unwrap_or(-1)),
3386− }
3387−}
3388−
3389−/// the branch the remote's HEAD names, its default bookmark (§7.8), asked of
3390−/// the remote with `git ls-remote --symref` (the git `git_fetch_subprocess`
3391−/// runs); `None` when HEAD names no branch (a detached HEAD)
3392−fn git_default_branch(
3393− git_dir: &std::path::Path,
3394− remote: &RemoteName,
3395−) -> Result<Option<String>, OpenError> {
3396− let exe = std::env::var("J_GIT").unwrap_or_else(|_| "git".to_string());
3397− let out = std::process::Command::new(exe)
3398− .arg("--git-dir")
3399− .arg(git_dir)
3400− .args(["ls-remote", "--symref", remote.as_str(), "HEAD"])
3401− .stdin(std::process::Stdio::null())
3402− .output()
3403− .map_err(|e| (1, format!("cannot run git ls-remote: {}", e)))?;
3404− if !out.status.success() {
3405− return Err((1, format!("cannot read the remote's default branch: {}", git_failure(&out))));
3406 }
1483 }
3407− // `ref: refs/heads/<branch>\tHEAD`
3408− Ok(String::from_utf8_lossy(&out.stdout).lines().find_map(|line| {
3409− let branch = line.strip_prefix("ref: refs/heads/")?.strip_suffix("\tHEAD")?;
3410− Some(branch.to_string())
3411− }))
3412}
1484}
3413
1485
3414enum OpKind {
1486enum OpKind {
3415 Normal,
1487 Normal,
3416− Undo,
1488+ /// the operation this undo discarded the view of
3417− Redo,
1489+ Undo { #[allow(dead_code)] undid: OperationId },
1490+ /// the undo this redo reversed
1491+ Redo { #[allow(dead_code)] redid: OperationId },
3418}
1492}
3419
1493
3420/// a marker parsed from an operation description (§7.7 kinds)
1494/// a marker parsed from an operation description (§7.7 kinds)
3421struct OpMarker {
1495struct OpMarker {
3422 kind: &'static str,
1496 kind: &'static str,
3423 id: OperationId,
1497 id: OperationId,
3424− /// jj 0.45 names the operation whose view was restored, rather than
1498+ /// whether the description is j's own exact marker (`undo: <id>` /
3425− /// the operation undone or the undo reversed.
1499+ /// `redo: <id>`); only those are reliably loadable for the walk
3426− restores_view: bool,
3427− /// Whether a legacy marker is j's exact `undo: <id>` / `redo: <id>`
3428− /// format, whose discarded-operation links the walk can follow.
3429 authored_by_j: bool,
1500 authored_by_j: bool,
3430}
1501}
3431
1502
3432−/// §7.7 kinds: new records use jj 0.45's restored-view descriptions. Older
1503+/// §7.7 kinds: `j` marks undo/redo operations as `undo: <opid>` /
1504+/// `redo: <opid>` in the description (0.45's Transaction cannot write
3433−/// j `undo: <id>` / `redo: <id>` and jj `undo operation <id>` /
1505+/// metadata tags); jj's own `undo operation <id>` / `redo operation <id>`
3434−/// `redo operation <id>` descriptions remain readable too.
1506+/// markers are treated the same way.
3435fn op_kind(meta: &jj_lib::op_store::OperationMetadata) -> OpKind {
1507fn op_kind(meta: &jj_lib::op_store::OperationMetadata) -> OpKind {
3436 match op_marker(meta) {
1508 match op_marker(meta) {
3437− Some(OpMarker { kind: "undo", .. }) => OpKind::Undo,
1509+ Some(OpMarker { kind: "undo", id, .. }) => OpKind::Undo { undid: id },
3438− Some(OpMarker { kind: "redo", .. }) => OpKind::Redo,
1510+ Some(OpMarker { kind: "redo", id, .. }) => OpKind::Redo { redid: id },
3439 _ => OpKind::Normal,
1511 _ => OpKind::Normal,
3440 }
1512 }
3441}
1513}
@@ -3443,33 +1515,20 @@ fn op_kind(meta: &jj_lib::op_store::OperationMetadata) -> OpKind {
3443fn op_marker(meta: &jj_lib::op_store::OperationMetadata) -> Option<OpMarker> {
1515fn op_marker(meta: &jj_lib::op_store::OperationMetadata) -> Option<OpMarker> {
3444 let desc = meta.description.trim();
1516 let desc = meta.description.trim();
3445 for kind in ["undo", "redo"] {
1517 for kind in ["undo", "redo"] {
3446− if let Some(rest) = desc.strip_prefix(&format!("{}: restore to operation ", kind)) {
3447− if let Some(id) = parse_op_id(rest.trim()) {
3448− return Some(OpMarker { kind, id, restores_view: true, authored_by_j: false });
3449− }
3450− }
3451 if let Some(rest) = desc.strip_prefix(&format!("{}:", kind)) {
1518 if let Some(rest) = desc.strip_prefix(&format!("{}:", kind)) {
3452 if let Some(id) = parse_op_id(rest.trim()) {
1519 if let Some(id) = parse_op_id(rest.trim()) {
3453− return Some(OpMarker { kind, id, restores_view: false, authored_by_j: true });
1520+ return Some(OpMarker { kind, id, authored_by_j: true });
3454 }
1521 }
3455 }
1522 }
3456 if let Some(rest) = desc.strip_prefix(&format!("{} operation", kind)) {
1523 if let Some(rest) = desc.strip_prefix(&format!("{} operation", kind)) {
3457 if let Some(id) = parse_op_id(rest.trim()) {
1524 if let Some(id) = parse_op_id(rest.trim()) {
3458− return Some(OpMarker { kind, id, restores_view: false, authored_by_j: false });
1525+ return Some(OpMarker { kind, id, authored_by_j: false });
3459 }
1526 }
3460 }
1527 }
3461 }
1528 }
3462 None
1529 None
3463}
1530}
3464
1531
3465−/// Whether `meta` is the operation `init` or `clone` recorded (§7.7), by its
3466−/// description, `init` or `clone URL`: no expression is described so, both
3467−/// words being reserved (§1.1).
3468−fn op_created_repo(meta: &jj_lib::op_store::OperationMetadata) -> bool {
3469− let desc = meta.description.trim();
3470− desc == "init" || desc.starts_with("clone ")
3471−}
3472−
3473/// a full-length (or prefix) hex operation id
1532/// a full-length (or prefix) hex operation id
3474fn parse_op_id(s: &str) -> Option<OperationId> {
1533fn parse_op_id(s: &str) -> Option<OperationId> {
3475 if s.is_empty() || !s.chars().all(|c| c.is_ascii_hexdigit()) {
1534 if s.is_empty() || !s.chars().all(|c| c.is_ascii_hexdigit()) {
@@ -3495,42 +1554,6 @@ fn op_parent_id(
3495 Ok(op.parent_ids().first().cloned())
1554 Ok(op.parent_ids().first().cloned())
3496}
1555}
3497
1556
3498−/// A restore operation represents an earlier view. Follow that view to a
3499−/// normal operation for useful confirmation text without changing undo's
3500−/// navigation or its jj-compatible metadata.
3501−fn restored_operation_description(
3502− loader: &jj_lib::repo::RepoLoader,
3503− op: &jj_lib::operation::Operation,
3504−) -> String {
3505− let mut represented = op.clone();
3506− let mut seen = BTreeSet::new();
3507− while seen.insert(represented.id().clone()) {
3508− let Some(marker) = op_marker(represented.metadata()) else { break };
3509− let target = if marker.restores_view {
3510− Some(marker.id)
3511− } else if marker.authored_by_j {
3512− // A legacy undo names what it discarded; a legacy redo names
3513− // the undo it reversed. Either restored its target's parent.
3514− op_parent_id(loader, &marker.id).ok().flatten()
3515− } else {
3516− // Unknown legacy jj history is best identified by its actual
3517− // description instead of guessing which operation it applied.
3518− None
3519− };
3520− let Some(target) = target else { break };
3521− // Missing historical metadata must not prevent an otherwise valid
3522− // recovery operation merely because its summary cannot be expanded.
3523− let Ok(target_op) = block_on(loader.load_operation(&target)) else { break };
3524− represented = target_op;
3525− }
3526− let description = represented.metadata().description.lines().next().unwrap_or("").trim();
3527− if description.is_empty() {
3528− format!("operation {}", truncate_chars(&represented.id().hex(), 12))
3529− } else {
3530− terminal_text(description)
3531− }
3532−}
3533−
3534/// parse the push-record list (§7.6): records of shape {id, name} set a
1557/// parse the push-record list (§7.6): records of shape {id, name} set a
3535/// bookmark, records of shape {delete} remove one; duplicates merge, the same
1558/// bookmark, records of shape {delete} remove one; duplicates merge, the same
3536/// name with different effects crashes (exit 1)
1559/// name with different effects crashes (exit 1)
@@ -3597,11 +1620,6 @@ fn parse_push_records(
3597 let rec = vis.commits.get(&change_id).ok_or_else(|| {
1620 let rec = vis.commits.get(&change_id).ok_or_else(|| {
3598 (1, format!("push: `@{}` is not a visible commit", change_id))
1621 (1, format!("push: `@{}` is not a visible commit", change_id))
3599 })?;
1622 })?;
3600− // jj's root has no Git object: its all-zero commit id is
3601− // interpreted by `git push` as a deletion, not a target.
3602− if rec.commit.id() == repo.store().root_commit_id() {
3603− return Err((1, "push: the root commit cannot be pushed".to_string()));
3604− }
3605 out.push((name, Some(rec.commit.id().clone())));
1623 out.push((name, Some(rec.commit.id().clone())));
3606 }
1624 }
3607 None => {
1625 None => {
@@ -3621,190 +1639,68 @@ fn parse_push_records(
3621 Ok(out)
1639 Ok(out)
3622}
1640}
3623
1641
3624−/// §7.6: refuse to move a bookmark whose current target is immutable to a
3625−/// commit that does not descend from that target, or to delete it while no
3626−/// bookmark left on `origin` after the push reaches that target; and refuse
3627−/// to send commits with unresolved files or empty descriptions. Returns
1642+/// §7.6: refuse to send commits with unresolved files or empty descriptions,
3628−/// whether such a delete is allowed only because a record's new target
1643+/// and refuse to move/delete a bookmark whose current target is immutable
3629−/// reaches it, so the deletes must wait for the updates (push_to_origin),
1644+/// away from a descendant of that target
3630−/// and refuses a push whose deletes would wait for a bookmark git cannot
3631−/// create while one of them is still there
3632fn check_push_records(
1645fn check_push_records(
3633 records: &[(String, Option<CommitId>)],
1646 records: &[(String, Option<CommitId>)],
3634− repo: &Arc<ReadonlyRepo>,
3635 vis: &VisibleRepo,
1647 vis: &VisibleRepo,
3636− immutable: &BTreeSet<String>,
1648+ repo: &Arc<ReadonlyRepo>,
3637−) -> Result<bool, OpenError> {
1649+) -> Result<(), OpenError> {
3638 let store = repo.store().clone();
1650 let store = repo.store().clone();
3639− let origin = RemoteName::new("origin");
3640− // the records' new targets, which the push sends and leaves on `origin`
3641− let new_targets: Vec<CommitId> = records.iter().filter_map(|(_, t)| t.clone()).collect();
3642− // the targets of the other bookmarks left on `origin` after the push:
3643− // those no record names, where they are as of the last fetch or push;
3644− // computed for the first delete that needs them
3645− let mut unnamed: Option<Vec<CommitId>> = None;
3646− // the deletes that only records' new targets keep reached
3647− let mut waiting: Vec<&str> = Vec::new();
3648 for (name, target) in records {
1651 for (name, target) in records {
1652+ let mut seeds: Vec<String> = Vec::new();
1653+ if let Some(commit_id) = target {
1654+ let commit = block_on(store.get_commit_async(commit_id))
1655+ .map_err(|e| (2, format!("cannot read a commit: {}", e)))?;
1656+ let change_id = commit.change_id().reverse_hex();
1657+ if vis.commits.contains_key(&change_id) {
1658+ seeds.push(change_id);
1659+ }
1660+ }
3649− let Some(current) = repo
1661+ if let Some(current) = repo
3650 .view()
1662 .view()
3651− .get_remote_bookmark(RefName::new(name).to_remote_symbol(origin))
1663+ .get_remote_bookmark(RefName::new(name).to_remote_symbol(RemoteName::new("origin")))
3652 .target
1664 .target
3653 .as_resolved()
1665 .as_resolved()
3654 .cloned()
1666 .cloned()
3655 .unwrap_or(None)
1667 .unwrap_or(None)
3656− else {
1668+ {
3657− continue;
3658− };
3659− let change_id = block_on(store.get_commit_async(&current))
1669+ let commit = block_on(store.get_commit_async(&current))
3660− .map_err(|e| (2, format!("cannot read a commit: {}", e)))?
1670+ .map_err(|e| (2, format!("cannot read a commit: {}", e)))?;
3661− .change_id()
3662− .reverse_hex();
1671+ let change_id = commit.change_id().reverse_hex();
3663− // the immutable set holds visible commits: a target rewritten here
1672+ if vis.commits.contains_key(&change_id) {
3664− // since the last fetch or push is hidden, and not in it although the
1673+ seeds.push(change_id);
3665− // visible rewrite carries its change id (and its label)
1674+ }
1675+ }
1676+ let ancestors = ancestors_of_change_ids(vis, &seeds);
1677+ for change_id in &ancestors {
3666− let visible = vis.commits.get(&change_id).is_some_and(|c| c.commit.id() == &current);
1678+ let rec = vis.commits.get(change_id).expect("ancestors are stored");
3667− if !visible || !immutable.contains(&change_id) {
1679+ for (_path, value) in rec.commit.tree().entries() {
3668− continue;
1680+ let value = value.map_err(|e| (2, format!("cannot read a tree: {}", e)))?;
3669− }
1681+ if !value.is_resolved() {
3670− match target {
3671− None => {
3672− // a delete loses no history while another bookmark still
3673− // reaches the commit: a merged feature, a second name. When
3674− // only a bookmark this push sets reaches it (`rename`), the
3675− // remote may reject that update and accept the delete, so
3676− // the delete waits for the update to be accepted
3677− let unnamed = unnamed.get_or_insert_with(|| {
3678− let named: BTreeSet<&str> = records.iter().map(|(n, _)| n.as_str()).collect();
3679− repo.view()
3680− .remote_bookmarks(origin)
3681− .filter(|(n, _)| !named.contains(n.as_str()))
3682− .flat_map(|(_, remote_ref)| remote_ref.target.added_ids())
3683− .cloned()
3684− .collect()
3685− });
3686− let reached = |heads: &[CommitId]| {
3687− ResolvedRevsetExpression::commits(vec![current.clone()])
3688− .intersection(&ResolvedRevsetExpression::commits(heads.to_vec()).ancestors())
3689− .evaluate(repo.as_ref())
3690− .and_then(|r| r.is_empty())
3691− .map(|empty| !empty)
3692− .map_err(|e| (2, format!("cannot read the index: {}", e)))
3693− };
3694− if !reached(unnamed)? {
3695− if !reached(&new_targets)? {
3696− return Err((
3697− 1,
3698− format!(
3699− "push: `{}` is on the immutable commit `@{}` and cannot be deleted while no bookmark left on origin reaches it",
3700− name, change_id
3701− ),
3702− ));
3703− }
3704− waiting.push(name);
3705− }
3706− }
3707− Some(new) => {
3708− let descends = block_on(repo.index().is_ancestor(&current, new))
3709− .map_err(|e| (2, format!("cannot read the index: {}", e)))?;
3710− if !descends {
3711− return Err((
3712− 1,
3713− format!(
3714− "push: `{}` is on the immutable commit `@{}` and can only move to a descendant of it",
3715− name, change_id
3716− ),
3717− ));
3718− }
3719− }
3720− }
3721− }
3722− // git cannot hold a bookmark and one under its name at once (`master`
3723− // and `master/legacy`), so while the deletes wait, a first push that
3724− // creates a name nesting with a deleted one is rejected and the deletes
3725− // are never sent. With a bookmark no record names on the waiting
3726− // delete's commit, nothing waits and it is one git push (§7.6)
3727− let deletes_wait = !waiting.is_empty();
3728− if deletes_wait {
3729− let nests = |a: &str, b: &str| b.strip_prefix(a).is_some_and(|rest| rest.starts_with('/'));
3730− for (deleted, _) in records.iter().filter(|(_, t)| t.is_none()) {
3731− for (set, _) in records.iter().filter(|(_, t)| t.is_some()) {
3732− if nests(deleted, set) || nests(set, deleted) {
3733− let keep = if waiting.contains(&deleted.as_str()) { deleted.as_str() } else { waiting[0] };
3734− let literal = crate::show::text_literal(keep);
3735− // the temporary label: the first of `tmp`, `tmp-1`, ...
3736− // that is no bookmark, here or on origin, no record
3737− // names, and nests with none of those, so pushing and
3738− // deleting it moves none of the user's bookmarks, and
3739− // this push, which names nothing nesting with it, no
3740− // longer waits on `keep`'s commit
3741− let taken: Vec<&str> = repo
3742− .view()
3743− .bookmarks()
3744− .map(|(n, _)| n.as_str())
3745− .chain(records.iter().map(|(n, _)| n.as_str()))
3746− .collect();
3747− let tmp = std::iter::once("tmp".to_string())
3748− .chain((1..).map(|i| format!("tmp-{i}")))
3749− .find(|t| !taken.iter().any(|n| n == t || nests(n, t) || nests(t, n)))
3750− .expect("finitely many names are taken");
3751− let tmp = crate::show::text_literal(&tmp);
3752 return Err((
1682 return Err((
3753 1,
1683 1,
3754 format!(
1684 format!(
3755− "push: `{deleted}` must stay on origin until `{set}` is accepted, and git cannot create `{set}` while `{deleted}` is there; first push a temporary label on `{keep}`'s commit, `push (label {tmp} (labelled {literal}))`, then this push, then `push (unlabel {tmp})`"
1685+ "push: commit `@{}` has unresolved files",
1686+ rec.change_id
3756 ),
1687 ),
3757 ));
1688 ));
3758 }
1689 }
3759 }
1690 }
3760− }
1691+ if rec.commit.description().trim().is_empty() && rec.commit.id() != store.root_commit_id() {
3761− }
3762− // the commits that would be sent: ancestors of the new targets that no
3763− // bookmark on the remote already reaches; a delete sends none
3764− if new_targets.is_empty() {
3765− return Ok(deletes_wait);
1692+ return Err((
3766− }
1693+ 1,
3767− let on_remote: Vec<CommitId> = repo
1694+ format!("push: commit `@{}` has an empty description", rec.change_id),
3768− .view()
1695+ ));
3769− .remote_bookmarks(origin)
3770− .flat_map(|(_, remote_ref)| remote_ref.target.added_ids())
3771− .cloned()
3772− .collect();
3773− let sent = ResolvedRevsetExpression::commits(on_remote)
3774− .range(&ResolvedRevsetExpression::commits(new_targets))
3775− .evaluate(repo.as_ref())
3776− .map_err(|e| (2, format!("cannot list the commits to send: {}", e)))?;
3777− let sent: Vec<Commit> = block_on(sent.stream().commits(&store).try_collect())
3778− .map_err(|e| (2, format!("cannot list the commits to send: {}", e)))?;
3779− for commit in &sent {
3780− let change_id = commit.change_id().reverse_hex();
3781− let mut unresolved_paths = Vec::new();
3782− for (path, value) in commit.tree().entries() {
3783− let value = value.map_err(|e| (2, format!("cannot read a tree: {}", e)))?;
3784− if !value.is_resolved() {
3785− unresolved_paths.push(path.as_internal_file_string().to_string());
3786− }
3787− }
3788− if !unresolved_paths.is_empty() {
3789− let mut paths = unresolved_paths.iter().take(5)
3790− .map(|path| format!("`{}`", terminal_text(path)))
3791− .collect::<Vec<_>>().join(", ");
3792− if unresolved_paths.len() > 5 {
3793− paths.push_str(&format!(" and {} more", unresolved_paths.len() - 5));
3794 }
1696 }
3795− return Err((1, format!(
3796− "push: commit `@{change_id}` has unresolved files: {paths}; run `j conflicts` to inspect conflicts, then `j by @{change_id}` to edit this commit. Resolve the listed files, run `j id` to record the resolution, and check `j conflicts` before pushing again"
3797− )));
3798− }
3799− if commit.description().trim().is_empty() && commit.id() != store.root_commit_id() {
3800− return Err((1, format!("push: commit `@{}` has an empty description", change_id)));
3801 }
1697 }
3802 }
1698 }
3803− Ok(deletes_wait)
1699+ Ok(())
3804}
1700}
3805
1701
3806/// change-id closure of jj-parents from the seeds (all jj parents, not just
1702/// change-id closure of jj-parents from the seeds (all jj parents, not just
3807−/// the first), for the immutable set (§7.5)
1703+/// the first), for the push content checks
3808fn ancestors_of_change_ids(vis: &VisibleRepo, seeds: &[String]) -> BTreeSet<String> {
1704fn ancestors_of_change_ids(vis: &VisibleRepo, seeds: &[String]) -> BTreeSet<String> {
3809 let mut seen = BTreeSet::new();
1705 let mut seen = BTreeSet::new();
3810 let mut stack: Vec<String> = seeds.to_vec();
1706 let mut stack: Vec<String> = seeds.to_vec();
@@ -3825,11 +1721,23 @@ fn ancestors_of_change_ids(vis: &VisibleRepo, seeds: &[String]) -> BTreeSet<Stri
3825
1721
3826/// git-check-ref-format --branch rules
1722/// git-check-ref-format --branch rules
3827fn is_valid_git_branch_name(name: &str) -> bool {
1723fn is_valid_git_branch_name(name: &str) -> bool {
1724+ if name.is_empty()
3828− if name.starts_with('-') {
1725+ || name.starts_with('-')
1726+ || name.starts_with('.')
1727+ || name.ends_with('.')
1728+ || name.ends_with('/')
1729+ || name.ends_with(".lock")
1730+ || name.contains("..")
1731+ || name.contains("//")
1732+ || name.contains("@{")
1733+ || name == "@"
1734+ {
3829 return false;
1735 return false;
3830 }
1736 }
3831− let full_name = format!("refs/heads/{name}");
1737+ name.chars().all(|c| {
3832− gix::validate::reference::branch_name(full_name.as_str().into()).is_ok()
1738+ !(c.is_ascii_control()
1739+ || matches!(c, ' ' | '~' | '^' | ':' | '?' | '*' | '[' | '\\'))
1740+ })
3833}
1741}
3834
1742
3835// ----------------------------------------------------------------------
1743// ----------------------------------------------------------------------
@@ -3837,12 +1745,6 @@ fn is_valid_git_branch_name(name: &str) -> bool {
3837// ----------------------------------------------------------------------
1745// ----------------------------------------------------------------------
3838
1746
3839impl Backend for JjBackend {
1747impl Backend for JjBackend {
3840− fn validate_authoring(&self) -> Result<(), Crash> {
3841− let repo = self.current_repo();
3842− check_identity(repo.settings().user_name(), repo.settings().user_email())
3843− .map_err(|(_, message)| Crash::new(message))
3844− }
3845−
3846 fn visible_ids(&self) -> Vec<String> {
1748 fn visible_ids(&self) -> Vec<String> {
3847 match self.inner.visible.lock().unwrap().as_ref() {
1749 match self.inner.visible.lock().unwrap().as_ref() {
3848 Some(v) => v.commits.keys().cloned().collect(),
1750 Some(v) => v.commits.keys().cloned().collect(),
@@ -3877,93 +1779,38 @@ impl Backend for JjBackend {
3877 vis.and_then(|v| v.commits.get(id).map(|r| r.commit.has_conflict()))
1779 vis.and_then(|v| v.commits.get(id).map(|r| r.commit.has_conflict()))
3878 }
1780 }
3879
1781
3880− fn is_empty(&self, id: &str, parent: &str) -> Option<bool> {
1782+ fn is_empty(&self, id: &str) -> Option<bool> {
3881 let vis = self.inner.visible.lock().unwrap().clone();
1783 let vis = self.inner.visible.lock().unwrap().clone();
3882− // O(1): empty iff the tree id equals the parent's tree id
1784+ // O(1): empty iff the tree id equals the first parent's tree id
3883 vis.and_then(|v| {
1785 vis.and_then(|v| {
3884 let rec = v.commits.get(id)?;
1786 let rec = v.commits.get(id)?;
3885− let parent = v.commits.get(parent)?;
1787+ let parent = v.commits.get(&rec.first_parent)?;
3886 Some(rec.commit.tree_ids() == parent.commit.tree_ids())
1788 Some(rec.commit.tree_ids() == parent.commit.tree_ids())
3887 })
1789 })
3888 }
1790 }
3889
1791
3890− fn folds_case(&self) -> bool {
3891− // jj's checkout refuses a name whose file identity is that of
3892− // `.git` or `.jj` in the same directory (local_working_copy.rs), so
3893− // where `.JJ` is the workspace's own `.jj` it refuses `.GIT` too.
3894− // Where each spelling of a name gets its own inode number
3895− // (exfat-fuse) the identities differ and the checkout writes
3896− // `.GIT/x` into `.git`, so folding is told by how names resolve:
3897− // `.JJ` resolves, but the directory does not list it under that
3898− // spelling. A directory that cannot be listed is taken to fold
3899− // case, the answer that refuses more.
3900− *self.inner.folds_case.get_or_init(|| {
3901− use jj_lib::file_util::FileIdentity;
3902− let root = &self.inner.workspace_root;
3903− let Ok(upper) = FileIdentity::from_symlink_path(root.join(".JJ")) else {
3904− return false;
3905− };
3906− FileIdentity::from_symlink_path(root.join(".jj")).is_ok_and(|lower| lower == upper)
3907− || std::fs::read_dir(root).map_or(true, |mut entries| {
3908− !entries.any(|e| e.is_ok_and(|e| e.file_name() == ".JJ"))
3909− })
3910− })
3911− }
3912−
3913− fn name_fits(&self, name: &str) -> bool {
3914− if name.len() <= crate::repo::NAME_MAX {
3915− return true;
3916− }
3917− // a component, never a path: one with `/` would be created elsewhere
3918− if name.contains(['/', '\0']) {
3919− return false;
3920− }
3921− // no call tells whether the limit counts bytes (ext4) or UTF-16
3922− // units or characters (exFAT, NTFS, APFS), so the name is tried:
3923− // created in a directory of its own under `.jj`, which is then
3924− // removed, once per name and run
3925− let mut known = self.inner.long_names.lock().unwrap();
3926− if let Some(fits) = known.get(name) {
3927− return *fits;
3928− }
3929− let fits = match tempfile::Builder::new()
3930− .prefix("name-")
3931− .tempdir_in(self.inner.workspace_root.join(".jj"))
3932− {
3933− Ok(dir) => {
3934− let fits = std::fs::File::create(dir.path().join(name)).is_ok();
3935− drop(dir);
3936− fits
3937− }
3938− Err(_) => false,
3939− };
3940− known.insert(name.to_string(), fits);
3941− fits
3942− }
3943−
3944 fn ancestors_closed(&self, ids: &BTreeSet<String>) -> BTreeSet<String> {
1792 fn ancestors_closed(&self, ids: &BTreeSet<String>) -> BTreeSet<String> {
3945 let vis = self.inner.visible.lock().unwrap().clone();
1793 let vis = self.inner.visible.lock().unwrap().clone();
3946 let Some(vis) = vis else {
1794 let Some(vis) = vis else {
3947 return BTreeSet::new();
1795 return BTreeSet::new();
3948 };
1796 };
3949− // every jj parent, not just the first: rewriting a commit reached
1797+ let mut seen = BTreeSet::new();
3950− // only through a merge's second parent rewrites the merge too
3951− // (§7.5 step 3)
3952− let seeds: Vec<String> = ids.iter().cloned().collect();
1798+ let mut stack: Vec<String> = ids.iter().cloned().collect();
3953− ancestors_of_change_ids(&vis, &seeds)
1799+ while let Some(id) = stack.pop() {
1800+ if seen.insert(id.clone()) {
1801+ if let Some(rec) = vis.commits.get(&id) {
1802+ if !rec.first_parent.is_empty() {
1803+ stack.push(rec.first_parent.clone());
1804+ }
1805+ }
1806+ }
1807+ }
1808+ seen
3954 }
1809 }
3955
1810
3956 fn replay(&self, onto: &[Value], from: &[Value], to: &[Value]) -> Result<Vec<Value>, Crash> {
1811 fn replay(&self, onto: &[Value], from: &[Value], to: &[Value]) -> Result<Vec<Value>, Crash> {
3957− self.replay_snapshot(
3958− &Value::list(onto.to_vec()),
3959− &Value::list(from.to_vec()),
3960− &Value::list(to.to_vec()),
3961− )?.as_list().map(<[Value]>::to_vec)
3962− }
3963−
3964− fn replay_snapshot(&self, onto: &Value, from: &Value, to: &Value) -> Result<Value, Crash> {
3965 let store = self.current_repo().store().clone();
1812 let store = self.current_repo().store().clone();
3966− block_on(jj_replay(&store, &self.replay_cache, onto, from, to))
1813+ block_on(jj_replay(&store, onto, from, to))
3967 }
1814 }
3968}
1815}
3969
1816
@@ -3971,197 +1818,30 @@ impl Backend for JjBackend {
3971// jj three-way tree merge for `replay` (§7.3)
1818// jj three-way tree merge for `replay` (§7.3)
3972// ----------------------------------------------------------------------
1819// ----------------------------------------------------------------------
3973
1820
3974−/// Replay sees the same lists repeatedly: a parent's old and new files
3975−/// become each child's bases. Retain their validated trees by list identity,
3976−/// including its offset, without keeping discarded snapshots alive.
3977−#[derive(Default)]
3978−struct ReplayCache {
3979− trees: RefCell<HashMap<(*const Vec<Value>, usize), ReplayTree>>,
3980− empty_subtrees: RefCell<HashMap<TreeId, bool>>,
3981− next_sweep: Cell<usize>,
3982− blobs: BlobCache,
3983− entries: EntryCache,
3984−}
3985−
3986−struct ReplayTree {
3987− storage: Weak<Vec<Value>>,
3988− tree: MergedTree,
3989− /// A list read from a resolved tree, already in canonical path order.
3990− canonical: bool,
3991−}
3992−
3993−impl ReplayCache {
3994− fn remember(&self, list: &ListVal, tree: MergedTree, canonical: bool) {
3995− let mut trees = self.trees.borrow_mut();
3996− trees.insert(list.identity(), ReplayTree {
3997− storage: list.weak_storage(),
3998− tree,
3999− canonical,
4000− });
4001− if trees.len() >= self.next_sweep.get().max(256) {
4002− trees.retain(|_, t| t.storage.strong_count() != 0);
4003− self.next_sweep.set((trees.len() * 2).max(256));
4004− }
4005− }
4006−
4007− async fn tree_of(&self, store: &Arc<Store>, files: &Value) -> Result<(MergedTree, bool), Crash> {
4008− let Value::List(list) = files else {
4009− files.as_list()?;
4010− unreachable!();
4011− };
4012− if let Some(cached) = self.trees.borrow().get(&list.identity()) {
4013− // The weak reference keeps its allocation reserved, so another
4014− // list cannot reuse this address while the key is cached.
4015− if cached.storage.strong_count() != 0 {
4016− return Ok((cached.tree.clone(), cached.canonical));
4017− }
4018− }
4019− // Validate every new list before caching it, even when from == to:
4020− // an unchanged change must still reject malformed snapshots.
4021− let tree = build_tree(store, files).await?;
4022− self.remember(list, tree.clone(), false);
4023− Ok((tree, false))
4024− }
4025−
4026− async fn files_of(&self, store: &Arc<Store>, tree: &MergedTree) -> Result<Value, Crash> {
4027− let (files, reusable) = tree_to_files_inner(store, tree, &self.blobs, &self.entries).await?;
4028− let list = ListVal::new(files);
4029− if reusable && !self.has_empty_subtrees(tree).await? {
4030− self.remember(&list, tree.clone(), true);
4031− }
4032− Ok(Value::List(list))
4033− }
4034−
4035− /// Flattening a Git tree omits empty directories. Reusing such a tree
4036− /// would let a replay conflict with a directory absent from its input
4037− /// snapshot. Inspect directories once per tree, without rebuilding files.
4038− async fn has_empty_subtrees(&self, tree: &MergedTree) -> Result<bool, Crash> {
4039− let Some(root) = tree.tree_ids().as_resolved() else { return Ok(true) };
4040− if let Some(found) = self.empty_subtrees.borrow().get(root) {
4041− return Ok(*found);
4042− }
4043− let store = tree.store();
4044− let mut pending = vec![(RepoPathBuf::root(), root.clone())];
4045− let mut visited = BTreeSet::new();
4046− while let Some((dir, id)) = pending.pop() {
4047− if !visited.insert(id.clone()) {
4048− continue;
4049− }
4050− let known = self.empty_subtrees.borrow().get(&id).copied();
4051− if let Some(found) = known {
4052− if found {
4053− self.empty_subtrees.borrow_mut().insert(root.clone(), true);
4054− return Ok(true);
4055− }
4056− continue;
4057− }
4058− let loaded = store.get_tree(dir.clone(), &id).await
4059− .map_err(|e| Crash::new(format!("cannot read a tree: {}", e)))?;
4060− for entry in loaded.entries_non_recursive() {
4061− if let TreeValue::Tree(child) = entry.value() {
4062− if child == store.empty_tree_id() {
4063− self.empty_subtrees.borrow_mut().insert(root.clone(), true);
4064− return Ok(true);
4065− }
4066− pending.push((dir.join(entry.name()), child.clone()));
4067− }
4068− }
4069− }
4070− // Every subtree visited was nonempty all the way down. Sharing this
4071− // answer also avoids rescanning unchanged directories of later trees.
4072− self.empty_subtrees.borrow_mut().extend(visited.into_iter().map(|id| (id, false)));
4073− Ok(false)
4074− }
4075−}
4076−
4077async fn jj_replay(
1821async fn jj_replay(
4078 store: &Arc<Store>,
1822 store: &Arc<Store>,
4079− cache: &ReplayCache,
4080− onto: &Value,
1823+ onto: &[Value],
4081− from: &Value,
1824+ from: &[Value],
4082− to: &Value,
1825+ to: &[Value],
4083−) -> Result<Value, Crash> {
1826+) -> Result<Vec<Value>, Crash> {
4084− let (onto_tree, onto_canonical) = cache.tree_of(store, onto).await?;
1827+ let onto_tree = build_tree(store, &Value::list(onto.to_vec())).await?;
4085− let (from_tree, from_canonical) = cache.tree_of(store, from).await?;
1828+ let from_tree = build_tree(store, &Value::list(from.to_vec())).await?;
4086− let (to_tree, to_canonical) = cache.tree_of(store, to).await?;
1829+ let to_tree = build_tree(store, &Value::list(to.to_vec())).await?;
4087− // any of the three may itself be conflicted (a snapshot with unresolved
1830+ let merge = Merge::from_removes_adds(
4088− // blobs, §7.3), so merge the trees the way jj's rebase does:
1831+ vec![from_tree.tree_ids().as_resolved().cloned().unwrap()],
4089− // MergedTree::merge flattens conflicted inputs into one merge, simplifies
1832+ vec![
1833+ onto_tree.tree_ids().as_resolved().cloned().unwrap(),
1834+ to_tree.tree_ids().as_resolved().cloned().unwrap(),
1835+ ],
1836+ );
4090− // it, and resolves file contents line-level with tree_merge::merge_trees,
1837+ // tree_merge::merge_trees resolves file contents line-level and keeps
4091− // keeping unresolvable paths as conflicts. Empty labels leave the result
1838+ // unresolvable paths as conflicts (tree_merge.rs:78)
4092− // unlabeled; the labels would not survive tree_to_files anyway.
1839+ let merged = jj_lib::tree_merge::merge_trees(store, merge)
4093− let merged = MergedTree::merge(Merge::from_removes_adds(
4094− vec![(from_tree.clone(), String::new())],
4095− vec![(onto_tree.clone(), String::new()), (to_tree.clone(), String::new())],
4096− ))
4097− .await
1840+ .await
4098− .map_err(|e| Crash::new(format!("replay: {}", e)))?;
1841+ .map_err(|e| Crash::new(format!("replay: {}", e)))?;
4099− refuse_file_directory_clash(&merged, &onto_tree, &from_tree, &to_tree).await?;
4100− // Reusing a resolved tree's list preserves replay's sorted output. A
4101− // constructed list may have another order, and conflicts may simplify
4102− // on reading, so neither can take this shortcut.
4103− for (files, tree, canonical) in [
4104− (onto, &onto_tree, onto_canonical),
4105− (to, &to_tree, to_canonical),
4106− (from, &from_tree, from_canonical),
4107− ] {
4108− if canonical && merged.tree_ids() == tree.tree_ids() {
4109− return Ok(files.clone());
4110− }
4111− }
4112 // conflicts stay as unresolved blobs (§7.3)
1842 // conflicts stay as unresolved blobs (§7.3)
4113− cache.files_of(store, &merged).await
1843+ let merged = MergedTree::new(store.clone(), merged, jj_lib::conflict_labels::ConflictLabels::unlabeled());
4114−}
1844+ tree_to_files(store, &merged, &BlobCache::default(), &EntryCache::default()).await
4115−
4116−/// Crash if the merge left a conflict with a file on one side and, on
4117−/// another, a directory `onto` or `to` holds at that path: one entry cannot
4118−/// list that directory's entries (§7.3). A conflict that `onto`, `from` or
4119−/// `to` already holds at the path is not made here: its sides, a directory
4120−/// side jj made earlier included, came in as an unresolved blob, and replay
4121−/// carries them beside the others, as when the child resolving that conflict
4122−/// is squashed into it.
4123−async fn refuse_file_directory_clash(
4124− merged: &MergedTree,
4125− onto: &MergedTree,
4126− from: &MergedTree,
4127− to: &MergedTree,
4128−) -> Result<(), Crash> {
4129− let value_at = async |tree: &MergedTree, path: &RepoPath| {
4130− tree.path_value(path).await.map_err(|e| Crash::new(format!("replay: {}", e)))
4131− };
4132− // walks only the unresolved part of the tree: nothing when it is resolved
4133− for (path, value) in merged.conflicts() {
4134− let value = value.map_err(|e| Crash::new(format!("replay: {}", e)))?;
4135− // sides that cancel are no sides
4136− let value = value.simplify();
4137− let has = |dir: bool| {
4138− value
4139− .adds()
4140− .any(|v| matches!(v, Some(t) if matches!(t, TreeValue::Tree(_)) == dir))
4141− };
4142− if !has(false) || !has(true) {
4143− continue;
4144− }
4145− let held = [
4146− value_at(onto, &path).await?,
4147− value_at(from, &path).await?,
4148− value_at(to, &path).await?,
4149− ];
4150− // an input's conflict at the path, as opposed to one inside the
4151− // directory there, which leaves the path a directory in the input
4152− if held.iter().any(|h| !h.is_resolved() && !h.is_tree()) {
4153− continue;
4154− }
4155− let side = |dir: &MergedTreeValueT| {
4156− dir.is_tree() && value.adds().any(|v| v.is_some() && dir.iter().any(|h| h == v))
4157− };
4158− if side(&held[0]) || side(&held[2]) {
4159− let comps: Vec<String> =
4160− path.components().map(|c| c.as_internal_str().to_string()).collect();
4161− return Err(crate::domain::file_directory_clash(&comps));
4162− }
4163− }
4164− Ok(())
4165}
1845}
4166
1846
4167// ----------------------------------------------------------------------
1847// ----------------------------------------------------------------------
@@ -4182,107 +1862,6 @@ impl ConflictTreeValue {
4182 }
1862 }
4183}
1863}
4184
1864
4185−/// The stored trees a program's commits may still hold the files of, to
4186−/// write each such commit with the tree itself (§7.5 step 4). A list holds
4187−/// each conflict simplified (`merged_value_to_blob`), and the tree it was
4188−/// loaded from need not, so a tree built from the list could differ from
4189−/// the stored one where the files do not: a child `new` made would not be
4190−/// empty, as jj's own `new` leaves one.
4191−struct LoadedTrees<'a> {
4192− /// the stored commits the program's repo was loaded from
4193− visible: Option<&'a VisibleRepo>,
4194− /// the lists the lazy `files` of the program's commits have loaded, by
4195− /// identity: `c.files` takes the list out, so a commit given it, as
4196− /// `new` gives its child its parent's, holds the list itself
4197− lists: HashMap<(*const Vec<Value>, usize), Rc<ThunkVal>>,
4198−}
4199−
4200−impl<'a> LoadedTrees<'a> {
4201− /// The trees `repos`' commits were loaded with; nothing is read
4202− fn new(visible: Option<&'a VisibleRepo>, repos: &[&Value]) -> Result<LoadedTrees<'a>, Crash> {
4203− let mut lists = HashMap::new();
4204− for repo in repos {
4205− for commit in crate::repo::all_commits(repo)? {
4206− if let Value::Thunk(t) = crate::repo::files_of(&commit)? {
4207− if let (Some(_), Some(Value::List(list))) = (t.tree(), t.peek()) {
4208− lists.insert(list.identity(), t);
4209− }
4210− }
4211− }
4212− }
4213− Ok(LoadedTrees { visible, lists })
4214− }
4215−
4216− /// The tree to write for a commit on `parents` whose files are `files`:
4217− /// where they are still a list loaded from a stored tree, that tree as
4218− /// jj stored it, and otherwise the one `build_tree` makes of them; but
4219− /// where either lists the parents' files, the parents' tree
4220− fn tree_to_write(&self, repo: &dyn Repo, files: &Value, parents: &[CommitId]) -> Result<MergedTree, Crash> {
4221− let tree = self.tree_of(repo.store(), files)?;
4222− Ok(block_on(parents_tree_if_same(repo, parents, tree)))
4223− }
4224−
4225− fn tree_of(&self, store: &Arc<Store>, files: &Value) -> Result<MergedTree, Crash> {
4226− let loaded = match files {
4227− Value::Thunk(t) => Some(t),
4228− Value::List(list) => self.lists.get(&list.identity()),
4229− _ => None,
4230− };
4231− if let Some(t) = loaded {
4232− let stored = t.origin().zip(self.visible).and_then(|(id, vis)| vis.commits.get(id));
4233− if let Some(tree) = stored.map(|rec| rec.commit.tree()) {
4234− if t.tree() == Some(tree_name(&tree).as_str()) {
4235− return Ok(tree);
4236− }
4237− }
4238− }
4239− block_on(build_tree(store, &files.forced()?))
4240− }
4241−}
4242−
4243−/// The tree of `parents`, merged where there are several, as jj's emptiness
4244−/// takes it, where it lists the files `tree` lists, and otherwise `tree`
4245−/// (§7.5 step 4). A list holds each conflict less the sides that cancel
4246−/// (§7.3), and a tree built from it holds no labels, where a tree jj merged
4247−/// keeps both; so a commit left with its parent's files by `squash`,
4248−/// `rebase` or `abandon`, which build the files anew, got another tree for
4249−/// them: jj took it for a change at each conflicted path, the tree's empty
4250−/// mark (`is_empty`) did not show, and the checkout wrote the conflicts
4251−/// again with generic labels. So did a commit moved onto another tree for
4252−/// its files, which kept its own, and a snapshot that reverted the commit's
4253−/// change, which builds on the commit's tree (§7.4). Files with no conflict
4254−/// make one tree, so a resolved parents' tree is not compared, nor one of
4255−/// the same ids, and the comparison stops at the first path where the two
4256−/// differ. It only chooses between two trees for the same files, so where
4257−/// the parents' cannot be read, it is `tree`.
4258−async fn parents_tree_if_same(repo: &dyn Repo, parents: &[CommitId], tree: MergedTree) -> MergedTree {
4259− let mut commits = Vec::with_capacity(parents.len());
4260− for id in parents {
4261− match repo.store().get_commit_async(id).await {
4262− Ok(commit) => commits.push(commit),
4263− Err(_) => return tree,
4264− }
4265− }
4266− let Ok(parents_tree) = jj_lib::rewrite::merge_commit_trees(repo, &commits).await else {
4267− return tree;
4268− };
4269− // the same terms: the parents' tree, with jj's labels for them
4270− if parents_tree.tree_ids() == tree.tree_ids() {
4271− return parents_tree;
4272− }
4273− if parents_tree.tree_ids().is_resolved() {
4274− return tree;
4275− }
4276− let mut diff = parents_tree.diff_stream(&tree, &EverythingMatcher);
4277− while let Some(entry) = diff.next().await {
4278− match entry.values {
4279− Ok(Diff { before, after }) if value_as_read(&before) == value_as_read(&after) => {}
4280− _ => return tree,
4281− }
4282− }
4283− parents_tree
4284−}
4285−
4286async fn build_tree(store: &Arc<Store>, files: &Value) -> Result<MergedTree, Crash> {
1865async fn build_tree(store: &Arc<Store>, files: &Value) -> Result<MergedTree, Crash> {
4287 let entries = files
1866 let entries = files
4288 .as_list()
1867 .as_list()
@@ -4342,41 +1921,19 @@ async fn blob_to_tree_value(store: &Arc<Store>, content: &Value) -> Result<Merge
4342 return Err(Crash::new("persistence: malformed conflict blob"));
1921 return Err(Crash::new("persistence: malformed conflict blob"));
4343 }
1922 }
4344 // jj order: [add, (remove, add)*] → Merge with interleaved adds
1923 // jj order: [add, (remove, add)*] → Merge with interleaved adds
4345− // and removes; each side keeps its absence, its own file type or
1924+ // and removes
4346− // its directory (§7.3), so the tree written is the one the
4347− // conflict was read from
4348 let mut values: Vec<Option<TreeValue>> = Vec::new();
1925 let mut values: Vec<Option<TreeValue>> = Vec::new();
4349 for side in sides {
1926 for side in sides {
4350− let Some(side) = side else {
4351− values.push(None);
4352− continue;
4353− };
4354− let value = if let Some(hex) = &side.tree {
4355− TreeValue::Tree(
4356− TreeId::try_from_hex(hex)
4357− .ok_or_else(|| Crash::new("persistence: bad tree id"))?,
4358− )
4359− } else if side.kind == BlobKind::Symlink {
4360− let target = std::str::from_utf8(&side.bytes)
4361− .map_err(|_| Crash::new("persistence: symlink target is not UTF-8"))?;
4362− let id = store
4363− .write_symlink(&RepoPathBuf::root(), target)
4364− .await
4365− .map_err(|e| Crash::new(format!("cannot write a symlink: {}", e)))?;
4366− TreeValue::Symlink(id)
4367− } else {
4368− let mut slice: &[u8] = side.bytes.as_ref();
1927+ let mut slice: &[u8] = side.as_ref();
4369− let id = store
1928+ let id = store
4370− .write_file(&RepoPathBuf::root(), &mut slice)
1929+ .write_file(&RepoPathBuf::root(), &mut slice)
4371− .await
1930+ .await
4372− .map_err(|e| Crash::new(format!("cannot write a file: {}", e)))?;
1931+ .map_err(|e| Crash::new(format!("cannot write a file: {}", e)))?;
4373− TreeValue::File {
1932+ values.push(Some(TreeValue::File {
4374− id,
1933+ id,
4375− executable: side.kind == BlobKind::Executable,
1934+ executable: b.kind == BlobKind::Executable,
4376− copy_id: CopyId::placeholder(),
1935+ copy_id: CopyId::placeholder(),
4377− }
1936+ }));
4378− };
4379− values.push(Some(value));
4380 }
1937 }
4381 let mut adds = Vec::new();
1938 let mut adds = Vec::new();
4382 let mut removes = Vec::new();
1939 let mut removes = Vec::new();
@@ -4406,49 +1963,34 @@ fn to_repo_path(path: &[String]) -> Result<RepoPathBuf, Crash> {
4406// jj trees -> values
1963// jj trees -> values
4407// ----------------------------------------------------------------------
1964// ----------------------------------------------------------------------
4408
1965
4409−/// Blob contents keyed by file id, shared while snapshots hold their bytes.
1966+/// blob contents keyed by file id, shared across the whole repo load: most
4410−/// Weak entries let discarded replay results release their conflict sides.
1967+/// files are unchanged between commits, so inflating each blob once (instead
1968+/// of once per commit that references it) keeps loading linear in the
1969+/// repository's unique data rather than commits × files
4411#[derive(Clone, Default)]
1970#[derive(Clone, Default)]
4412−struct BlobCache {
4413− bytes: Rc<RefCell<HashMap<FileId, Weak<Vec<u8>>>>>,
1971+struct BlobCache(Rc<RefCell<HashMap<FileId, Rc<Vec<u8>>>>>);
4414− next_sweep: Rc<Cell<usize>>,
4415−}
4416
1972
4417impl BlobCache {
1973impl BlobCache {
4418 fn get(&self, id: &FileId) -> Option<Rc<Vec<u8>>> {
1974 fn get(&self, id: &FileId) -> Option<Rc<Vec<u8>>> {
4419− self.bytes.borrow().get(id).and_then(Weak::upgrade)
1975+ self.0.borrow().get(id).cloned()
4420 }
1976 }
4421 fn insert(&self, id: FileId, bytes: Rc<Vec<u8>>) {
1977 fn insert(&self, id: FileId, bytes: Rc<Vec<u8>>) {
4422− let mut cached = self.bytes.borrow_mut();
1978+ self.0.borrow_mut().insert(id, bytes);
4423− cached.insert(id, Rc::downgrade(&bytes));
4424− if cached.len() >= self.next_sweep.get().max(256) {
4425− cached.retain(|_, bytes| bytes.strong_count() != 0);
4426− self.next_sweep.set((cached.len() * 2).max(256));
4427− }
4428 }
1979 }
4429}
1980}
4430
1981
4431−/// Whole file entries keyed by (path, blob identity), shared by loaded
1982+/// whole file *entries* ({path, content}) keyed by (path, blob identity),
4432−/// snapshots and replays. Weak references let transient replay results go
1983+/// shared across the whole repo load: an unchanged file is the same Value in
4433−/// away while unchanged entries still held by a snapshot are reused.
1984+/// every commit, so it is built once and cloned (an Rc bump) thereafter
4434#[derive(Clone, Default)]
1985#[derive(Clone, Default)]
4435−struct EntryCache {
1986+struct EntryCache(Rc<RefCell<HashMap<String, Value>>>);
4436− entries: Rc<RefCell<HashMap<String, Weak<crate::value::RecordMap>>>>,
4437− next_sweep: Rc<Cell<usize>>,
4438−}
4439
1987
4440impl EntryCache {
1988impl EntryCache {
4441 fn get(&self, key: &str) -> Option<Value> {
1989 fn get(&self, key: &str) -> Option<Value> {
4442− self.entries.borrow().get(key).and_then(Weak::upgrade).map(Value::Record)
1990+ self.0.borrow().get(key).cloned()
4443 }
1991 }
4444 fn insert(&self, key: String, entry: Value) {
1992 fn insert(&self, key: String, entry: Value) {
4445− let Value::Record(record) = entry else { return };
4446− let mut entries = self.entries.borrow_mut();
1993+ self.0.borrow_mut().insert(key, entry);
4447− entries.insert(key, Rc::downgrade(&record));
4448− if entries.len() >= self.next_sweep.get().max(256) {
4449− entries.retain(|_, entry| entry.strong_count() != 0);
4450− self.next_sweep.set((entries.len() * 2).max(256));
4451− }
4452 }
1994 }
4453}
1995}
4454
1996
@@ -4476,64 +2018,20 @@ async fn read_file_bytes(
4476 Ok(buf)
2018 Ok(buf)
4477}
2019}
4478
2020
4479−/// one side of a conflict as jj stores it: absent (`None`, a deletion
2021+async fn side_to_bytes(
4480−/// conflict), or content with that side's own file type (§7.3)
4481−async fn conflict_side(
4482 store: &Arc<Store>,
2022 store: &Arc<Store>,
4483 path: &RepoPath,
2023 path: &RepoPath,
4484 v: &Option<TreeValue>,
2024 v: &Option<TreeValue>,
4485 cache: &BlobCache,
2025 cache: &BlobCache,
4486−) -> Result<Option<ConflictSide>, Crash> {
2026+) -> Result<Rc<Vec<u8>>, Crash> {
4487 match v {
2027 match v {
4488− None => Ok(None),
4489− Some(TreeValue::File { id, executable, .. }) => Ok(Some(ConflictSide {
4490− kind: if *executable {
4491− BlobKind::Executable
4492− } else {
4493− BlobKind::Regular
4494− },
4495− bytes: read_file_bytes(store, path, id, cache).await?,
2028+ Some(TreeValue::File { id, .. }) => read_file_bytes(store, path, id, cache).await,
4496− tree: None,
4497− })),
4498− Some(TreeValue::Symlink(id)) => {
2029+ Some(TreeValue::Symlink(id)) => store
4499− let target = store
4500− .read_symlink(path, id)
2030+ .read_symlink(path, id)
4501− .await
2031+ .await
2032+ .map(|s| Rc::new(s.into_bytes()))
4502− .map_err(|e| Crash::new(format!("cannot read a symlink: {}", e)))?;
2033+ .map_err(|e| Crash::new(format!("cannot read a symlink: {}", e))),
4503− Ok(Some(ConflictSide {
4504− kind: BlobKind::Symlink,
4505− bytes: Rc::new(target.into_bytes()),
4506− tree: None,
4507− }))
4508− }
4509− // a directory side, in a conflict between a file and a directory,
4510− // reads as empty content but keeps its tree, so writing the conflict
4511− // back cannot put an empty file where the directory was
4512− Some(TreeValue::Tree(id)) => Ok(Some(ConflictSide {
4513− kind: BlobKind::Regular,
4514− bytes: Rc::new(Vec::new()),
2034+ _ => Ok(Rc::new(Vec::new())),
4515− tree: Some(id.hex()),
4516− })),
4517− // a submodule side reads as an empty file, as a resolved one does in
4518− // tree_value_to_blob (§12)
4519− Some(TreeValue::GitSubmodule(_)) => Ok(Some(ConflictSide::regular(&[]))),
4520− }
4521−}
4522−
4523−/// The value a list reads where a tree holds `value` (§7.3): a conflict less
4524−/// each pair of sides that cancel, one added and one removed the same, as jj
4525−/// simplifies a file conflict to write it out. jj keeps every term of a
4526−/// merge it cannot resolve, pairs that cancel included, so a replayed
4527−/// commit's conflict inherited from its parent was not the parent's, and
4528−/// each commit a rebase replayed above took two more sides for it. Where
4529−/// only directory sides would be left, the value is kept whole: jj keeps it
4530−/// as one conflict at the path, and the simplified one would be a conflict
4531−/// between directories, which jj merges entry by entry. Two values read
4532−/// the same are the same entry of a list.
4533−fn value_as_read(value: &MergedTreeValueT) -> std::borrow::Cow<'_, MergedTreeValueT> {
4534− match (!value.is_resolved()).then(|| value.simplify()).filter(|v| !v.is_tree()) {
4535− Some(simplified) => std::borrow::Cow::Owned(simplified),
4536− None => std::borrow::Cow::Borrowed(value),
4537 }
2035 }
4538}
2036}
4539
2037
@@ -4543,9 +2041,6 @@ async fn merged_value_to_blob(
4543 value: &jj_lib::backend::MergedTreeValue,
2041 value: &jj_lib::backend::MergedTreeValue,
4544 cache: &BlobCache,
2042 cache: &BlobCache,
4545) -> Result<Value, Crash> {
2043) -> Result<Value, Crash> {
4546− // the conflict less the sides that cancel (§7.3)
4547− let read = value_as_read(value);
4548− let value = read.as_ref();
4549 if let Some(v) = value.as_resolved() {
2044 if let Some(v) = value.as_resolved() {
4550 return match v {
2045 return match v {
4551 Some(tv) => tree_value_to_blob(store, path, tv).await,
2046 Some(tv) => tree_value_to_blob(store, path, tv).await,
@@ -4556,11 +2051,11 @@ async fn merged_value_to_blob(
4556 };
2051 };
4557 }
2052 }
4558 // conflict sides, jj order: [add, (remove, add)*] (§7.3)
2053 // conflict sides, jj order: [add, (remove, add)*] (§7.3)
4559− let mut sides: Vec<Option<ConflictSide>> = Vec::new();
2054+ let mut sides: Vec<Rc<Vec<u8>>> = Vec::new();
4560 for (i, add) in value.adds().enumerate() {
2055 for (i, add) in value.adds().enumerate() {
4561− sides.push(conflict_side(store, path, add, cache).await?);
2056+ sides.push(side_to_bytes(store, path, add, cache).await?);
4562 if let Some(remove) = value.get_remove(i) {
2057 if let Some(remove) = value.get_remove(i) {
4563− sides.push(conflict_side(store, path, remove, cache).await?);
2058+ sides.push(side_to_bytes(store, path, remove, cache).await?);
4564 }
2059 }
4565 }
2060 }
4566 Ok(Value::Blob(Rc::new(BlobVal {
2061 Ok(Value::Blob(Rc::new(BlobVal {
@@ -4612,9 +2107,8 @@ async fn tree_value_to_blob(
4612 content: BlobContent::Resolved(Rc::new(target.into_bytes())),
2107 content: BlobContent::Resolved(Rc::new(target.into_bytes())),
4613 })))
2108 })))
4614 }
2109 }
4615− // MergedTree::entries recurses into a resolved directory, and a
2110+ // a conflicted directory is emitted without its children by
4616− // directory side of a conflict goes through conflict_side, so this is
4617− // a submodule, which §12 puts out of scope
2111+ // MergedTree::entries; §12 puts such repairs out of scope
4618 TreeValue::Tree(_) | TreeValue::GitSubmodule(_) => Ok(Value::Blob(Rc::new(BlobVal {
2112 TreeValue::Tree(_) | TreeValue::GitSubmodule(_) => Ok(Value::Blob(Rc::new(BlobVal {
4619 kind: BlobKind::Regular,
2113 kind: BlobKind::Regular,
4620 content: BlobContent::Resolved(Rc::new(Vec::new())),
2114 content: BlobContent::Resolved(Rc::new(Vec::new())),
@@ -4628,27 +2122,9 @@ async fn tree_to_files(
4628 cache: &BlobCache,
2122 cache: &BlobCache,
4629 entries: &EntryCache,
2123 entries: &EntryCache,
4630) -> Result<Vec<Value>, Crash> {
2124) -> Result<Vec<Value>, Crash> {
4631− tree_to_files_inner(store, tree, cache, entries).await.map(|(files, _)| files)
4632−}
4633−
4634−/// Whether these files can reuse the original tree in replay. Reading a
4635−/// conflict simplifies it, and an unsupported git submodule reads as an
4636−/// empty file, so their original trees cannot represent the resulting list.
4637−async fn tree_to_files_inner(
4638− store: &Arc<Store>,
4639− tree: &MergedTree,
4640− cache: &BlobCache,
4641− entries: &EntryCache,
4642−) -> Result<(Vec<Value>, bool), Crash> {
4643 let mut out = Vec::new();
2125 let mut out = Vec::new();
4644− let mut reusable = tree.tree_ids().is_resolved();
4645 for (path, value) in tree.entries() {
2126 for (path, value) in tree.entries() {
4646 let value = value.map_err(|e| Crash::new(format!("cannot read a tree entry: {}", e)))?;
2127 let value = value.map_err(|e| Crash::new(format!("cannot read a tree entry: {}", e)))?;
4647− reusable &= match value.as_resolved() {
4648− Some(Some(TreeValue::File { copy_id, .. })) => *copy_id == CopyId::placeholder(),
4649− Some(Some(TreeValue::Symlink(_))) => true,
4650− _ => false,
4651− };
4652 // cache key: the entry is fully determined by its path and the
2128 // cache key: the entry is fully determined by its path and the
4653 // identity of its (resolved) content; conflicts are not cached
2129 // identity of its (resolved) content; conflicts are not cached
4654 let key = match value.as_resolved() {
2130 let key = match value.as_resolved() {
@@ -4679,22 +2155,13 @@ async fn tree_to_files_inner(
4679 };
2155 };
4680 out.push(entry);
2156 out.push(entry);
4681 }
2157 }
4682− Ok((out, reusable))
2158+ Ok(out)
4683}
2159}
4684
2160
4685// ----------------------------------------------------------------------
2161// ----------------------------------------------------------------------
4686// value helpers
2162// value helpers
4687// ----------------------------------------------------------------------
2163// ----------------------------------------------------------------------
4688
2164
4689−/// The name of a stored tree that a commit's lazy `files` carry
4690−/// (`ThunkVal::tree`): its tree ids, one per term of a conflicted tree. The
4691−/// list `tree_to_files` makes depends on nothing else, so two trees of one
4692−/// name list the same entries.
4693−fn tree_name(tree: &MergedTree) -> String {
4694− let ids: Vec<String> = tree.tree_ids().iter().map(|id| id.hex()).collect();
4695− ids.join(",")
4696−}
4697−
4698fn commit_id_of(commit: &Value) -> Result<String, Crash> {
2165fn commit_id_of(commit: &Value) -> Result<String, Crash> {
4699 match commit.field("id")? {
2166 match commit.field("id")? {
4700 Value::Id(i) => Ok(i.to_string()),
2167 Value::Id(i) => Ok(i.to_string()),
@@ -4722,13 +2189,15 @@ fn new_id_set(repo: &Value) -> Result<BTreeSet<String>, Crash> {
4722// building the Repo value from the visible map (§7.2)
2189// building the Repo value from the visible map (§7.2)
4723// ----------------------------------------------------------------------
2190// ----------------------------------------------------------------------
4724
2191
4725−fn build_repo_value(vis: &VisibleRepo, focus_change: &str, replay_cache: &Rc<ReplayCache>) -> Result<Value, OpenError> {
2192+fn build_repo_value(vis: &VisibleRepo, focus_change: &str) -> Result<Value, OpenError> {
4726 let root_rec = vis
2193 let root_rec = vis
4727 .commits
2194 .commits
4728 .get(ROOT_ID)
2195 .get(ROOT_ID)
4729 .ok_or_else(|| (2, "no root commit".to_string()))?;
2196 .ok_or_else(|| (2, "no root commit".to_string()))?;
4730 let store = root_rec.commit.store().clone();
2197 let store = root_rec.commit.store().clone();
2198+ let cache = BlobCache::default();
2199+ let entries = EntryCache::default();
4731− let mut subtree = build_subtree(vis, root_rec, &store, replay_cache)?;
2200+ let mut subtree = build_subtree(vis, root_rec, &store, &cache, &entries)?;
4732 sort_subtree(&mut subtree, vis);
2201 sort_subtree(&mut subtree, vis);
4733 let top = Value::record(&[
2202 let top = Value::record(&[
4734 ("children", subtree.field("children").map_err(|e| (2, e.msg))?),
2203 ("children", subtree.field("children").map_err(|e| (2, e.msg))?),
@@ -4745,20 +2214,19 @@ fn build_subtree(
4745 vis: &VisibleRepo,
2214 vis: &VisibleRepo,
4746 rec: &StoredCommit,
2215 rec: &StoredCommit,
4747 store: &Arc<Store>,
2216 store: &Arc<Store>,
4748− replay_cache: &Rc<ReplayCache>,
2217+ cache: &BlobCache,
2218+ entries: &EntryCache,
4749) -> Result<Value, OpenError> {
2219) -> Result<Value, OpenError> {
4750 // the files list is lazy (§7.2): most commits are never inspected, so
2220 // the files list is lazy (§7.2): most commits are never inspected, so
4751− // their file entries are materialized only on first `field("files")`.
2221+ // their file entries are materialized only on first `field("files")`
4752− // Tagged with the change id, it tells the renderer when the O(1)
4753− // `has_conflict`/`is_empty` answers still describe the commit's files;
4754− // named by its tree, it lets persistence compare it unread (§7.5)
4755 let files_thunk = {
2222 let files_thunk = {
4756 let store = store.clone();
2223 let store = store.clone();
4757 let tree = rec.commit.tree();
2224 let tree = rec.commit.tree();
4758− let name = Some(tree_name(&tree));
2225+ let cache = cache.clone();
4759− let replay_cache = replay_cache.clone();
2226+ let entries = entries.clone();
4760− Value::Thunk(Rc::new(crate::value::ThunkVal::stored(rec.change_id.clone(), name, move || {
2227+ Value::Thunk(Rc::new(crate::value::ThunkVal::new(move || {
4761− block_on(replay_cache.files_of(&store, &tree))
2228+ let files = block_on(tree_to_files(&store, &tree, &cache, &entries))?;
2229+ Ok(Value::list(files))
4762 })))
2230 })))
4763 };
2231 };
4764 let labels: Vec<Value> = vis
2232 let labels: Vec<Value> = vis
@@ -4779,7 +2247,7 @@ fn build_subtree(
4779 if let Some(kids) = vis.children.get(&rec.change_id) {
2247 if let Some(kids) = vis.children.get(&rec.change_id) {
4780 for kid in kids {
2248 for kid in kids {
4781 if let Some(krec) = vis.commits.get(kid) {
2249 if let Some(krec) = vis.commits.get(kid) {
4782− children.push(build_subtree(vis, krec, store, replay_cache)?);
2250+ children.push(build_subtree(vis, krec, store, cache, entries)?);
4783 }
2251 }
4784 }
2252 }
4785 }
2253 }
@@ -4814,165 +2282,3 @@ fn sort_subtree(subtree: &mut Value, vis: &VisibleRepo) {
4814 let root = subtree.field("root").expect("subtree has a root");
2282 let root = subtree.field("root").expect("subtree has a root");
4815 *subtree = Value::record(&[("children", Value::list(kids)), ("root", root)]);
2283 *subtree = Value::record(&[("children", Value::list(kids)), ("root", root)]);
4816}
2284}
4817−
4818−#[cfg(test)]
4819−mod tests {
4820− use super::*;
4821− use jj_lib::backend::SymlinkId;
4822− use jj_lib::conflict_labels::ConflictLabels;
4823−
4824− fn replay_store() -> (tempfile::TempDir, Arc<Store>) {
4825− let dir = tempfile::tempdir().unwrap();
4826− let (_, repo) = block_on(Workspace::init_colocated_git(
4827− &user_settings(None), dir.path(), gix::hash::Kind::Sha1,
4828− )).unwrap();
4829− (dir, repo.store().clone())
4830− }
4831−
4832− fn file(path: &str, content: &str) -> Value {
4833− Value::record(&[
4834− ("path", Value::list(vec![Value::text(path)])),
4835− ("content", BlobVal::text_blob(content)),
4836− ])
4837− }
4838−
4839− fn loaded_files(store: &Arc<Store>, cache: &ReplayCache, files: Vec<Value>) -> Value {
4840− let tree = block_on(build_tree(store, &Value::list(files))).unwrap();
4841− block_on(cache.files_of(store, &tree)).unwrap()
4842− }
4843−
4844− #[test]
4845− fn replay_reuses_loaded_snapshots_and_unchanged_entries() {
4846− let (_dir, store) = replay_store();
4847− let cache = ReplayCache::default();
4848− let base = loaded_files(&store, &cache, vec![file("a", "base a"), file("b", "base b")]);
4849− let same = block_on(jj_replay(&store, &cache, &base, &base, &base)).unwrap();
4850− let (Value::List(before), Value::List(after)) = (&base, &same) else { panic!() };
4851− assert_eq!(before.identity(), after.identity(), "no-op replay must retain the snapshot");
4852−
4853− let onto = loaded_files(&store, &cache, vec![file("a", "onto"), file("b", "base b")]);
4854− let to = loaded_files(&store, &cache, vec![file("a", "base a"), file("b", "to")]);
4855− let merged = block_on(jj_replay(&store, &cache, &onto, &base, &to)).unwrap();
4856− let expected = Value::list(vec![file("a", "onto"), file("b", "to")]);
4857− assert!(crate::value::value_eq(&merged, &expected).unwrap());
4858− for (result, input) in [
4859− (&merged.as_list().unwrap()[0], &onto.as_list().unwrap()[0]),
4860− (&merged.as_list().unwrap()[1], &to.as_list().unwrap()[1]),
4861− ] {
4862− let (Value::Record(result), Value::Record(input)) = (result, input) else { panic!() };
4863− assert!(Rc::ptr_eq(result, input), "unchanged file entries must be shared");
4864− }
4865− }
4866−
4867− #[test]
4868− fn replay_cache_preserves_validation_ordering_and_list_offsets() {
4869− let (_dir, store) = replay_store();
4870− let cache = ReplayCache::default();
4871− let unsorted = Value::list(vec![file("b", "b"), file("a", "a")]);
4872− let empty = Value::list(vec![]);
4873− let sorted = block_on(jj_replay(&store, &cache, &unsorted, &empty, &empty)).unwrap();
4874− assert!(crate::value::value_eq(&sorted, &Value::list(vec![file("a", "a"), file("b", "b")])).unwrap());
4875−
4876− let Value::List(list) = &sorted else { panic!() };
4877− let tail = Value::List(list.skip(1));
4878− let replayed_tail = block_on(jj_replay(&store, &cache, &tail, &empty, &empty)).unwrap();
4879− assert!(crate::value::value_eq(&tail, &replayed_tail).unwrap());
4880− assert_eq!(replayed_tail.as_list().unwrap().len(), 1);
4881−
4882− let malformed = Value::list(vec![file("a", "one"), file("a", "two")]);
4883− let err = block_on(jj_replay(&store, &cache, &sorted, &malformed, &malformed)).unwrap_err();
4884− assert!(err.msg.contains("duplicate paths"), "{}", err.msg);
4885− }
4886−
4887− #[test]
4888− fn replay_ignores_empty_directories_not_represented_by_snapshots() {
4889− let (_dir, store) = replay_store();
4890− let cache = ReplayCache::default();
4891− let entry = |id| jj_lib::backend::Tree::from_sorted_entries(vec![
4892− (RepoPathComponentBuf::new("d".to_string()).unwrap(), TreeValue::Tree(id)),
4893− ]);
4894− let direct = block_on(store.write_tree(RepoPath::root(), entry(store.empty_tree_id().clone()))).unwrap();
4895− let nested = block_on(store.write_tree(RepoPath::root(), entry(direct.id().clone()))).unwrap();
4896− let empty = Value::list(vec![]);
4897− let to = Value::list(vec![file("d", "file")]);
4898− for tree in [direct, nested] {
4899− let tree = MergedTree::resolved(store.clone(), tree.id().clone());
4900− let onto = block_on(cache.files_of(&store, &tree)).unwrap();
4901− assert!(onto.as_list().unwrap().is_empty());
4902− let result = block_on(jj_replay(&store, &cache, &onto, &empty, &to)).unwrap();
4903− assert!(crate::value::value_eq(&result, &to).unwrap());
4904− }
4905− }
4906−
4907− #[test]
4908− fn replay_cache_does_not_keep_discarded_snapshots_alive() {
4909− let (_dir, store) = replay_store();
4910− let cache = ReplayCache::default();
4911− let files = loaded_files(&store, &cache, vec![file("a", "a")]);
4912− let Value::List(list) = &files else { panic!() };
4913− let storage = list.weak_storage();
4914− let Value::Record(entry) = &files.as_list().unwrap()[0] else { panic!() };
4915− let entry = Rc::downgrade(entry);
4916− drop(files);
4917− assert!(storage.upgrade().is_none());
4918− assert!(entry.upgrade().is_none());
4919−
4920− let bytes = Rc::new(vec![1, 2, 3]);
4921− let weak_bytes = Rc::downgrade(&bytes);
4922− let id = FileId::new(vec![1; 20]);
4923− cache.blobs.insert(id.clone(), bytes.clone());
4924− assert!(Rc::ptr_eq(&cache.blobs.get(&id).unwrap(), &bytes));
4925− drop(bytes);
4926− assert!(weak_bytes.upgrade().is_none());
4927− assert!(cache.blobs.get(&id).is_none());
4928− }
4929−
4930− #[test]
4931− fn user_settings_preserve_every_ascii_control_character() {
4932− let identity: String = (0u8..=127).map(char::from).collect();
4933− let text = format!(
4934− "[user]\nname = {}\nemail = {}\n",
4935− toml_string(&identity),
4936− toml_string(&identity)
4937− );
4938− let mut config = StackedConfig::with_defaults();
4939− config.add_layer(ConfigLayer::parse(ConfigSource::User, &text).unwrap());
4940− let settings = UserSettings::from_config(config).unwrap();
4941− assert_eq!(settings.user_name(), identity);
4942− assert_eq!(settings.user_email(), identity);
4943− }
4944−
4945− #[test]
4946− fn a_description_names_its_terms_whatever_their_labels_and_the_sides_that_cancel() {
4947− // what a checkout writes for a symlink/file conflict of five terms,
4948− // and for the three it simplifies to, which a snapshot may write
4949− // without writing the file again (§7.4)
4950− let link = Some(TreeValue::Symlink(SymlinkId::new(vec![1; 20])));
4951− let file = |n: u8| {
4952− let id = FileId::new(vec![n; 20]);
4953− Some(TreeValue::File { id, executable: false, copy_id: CopyId::placeholder() })
4954− };
4955− let five = Merge::from_vec(vec![link.clone(), link.clone(), file(2), file(1), link.clone()]);
4956− let three = five.simplify();
4957− assert_eq!(three.num_sides(), 2);
4958− let unlabeled = ConflictLabels::unlabeled();
4959− let written = five.describe(&unlabeled);
4960− let simpler = three.describe(&unlabeled);
4961− assert_ne!(written, simpler);
4962− let terms = described_terms(simpler.as_bytes());
4963− assert!(terms.is_some());
4964− assert_eq!(described_terms(written.as_bytes()), terms);
4965− let names = ["left (one)", "base", "middle", "base 2", "right"];
4966− let labels = ConflictLabels::from_vec(names.iter().map(|n| n.to_string()).collect());
4967− let labelled = five.describe(&labels);
4968− assert!(labelled.contains(" (left (one))\n"), "{}", labelled);
4969− assert_eq!(described_terms(labelled.as_bytes()), terms);
4970− // other sides, and text that is no description, are no such conflict
4971− let other = Merge::from_vec(vec![link.clone(), file(1), file(3)]).describe(&unlabeled);
4972− assert!(described_terms(other.as_bytes()).is_some_and(|t| Some(t) != terms));
4973− assert_eq!(described_terms(b"resolved\n"), None);
4974− assert_eq!(described_terms(written.replace("Conflict:", "CONFLICT:").as_bytes()), None);
4975− assert_eq!(described_terms(written.trim_end().as_bytes()), None);
4976− assert_eq!(described_terms(format!("{} Adding file with id 0101 and more\n", written).as_bytes()), None);
4977− }
4978−}
src/lex.rs+20−33
@@ -45,9 +45,6 @@ pub struct SpTok {
45 pub tok: Tok,
45 pub tok: Tok,
46 pub line: usize, // 1-based
46 pub line: usize, // 1-based
47 pub col: usize, // 1-based
47 pub col: usize, // 1-based
48− /// byte offsets of the token in the source: `start..end`
49− pub start: usize,
50− pub end: usize,
51}
48}
52
49
53#[derive(Debug)]
50#[derive(Debug)]
@@ -87,13 +84,6 @@ fn valid_label(s: &str) -> bool {
87pub fn lex(src: &str) -> Result<Vec<SpTok>, LexError> {
84pub fn lex(src: &str) -> Result<Vec<SpTok>, LexError> {
88 let mut toks: Vec<SpTok> = Vec::new();
85 let mut toks: Vec<SpTok> = Vec::new();
89 let chars: Vec<char> = src.chars().collect();
86 let chars: Vec<char> = src.chars().collect();
90− // byte offset of each char, and of the end: tokens carry byte spans so
91− // the parser can keep a lambda's source text (§5.2)
92− let bytes: Vec<usize> = src
93− .char_indices()
94− .map(|(b, _)| b)
95− .chain(std::iter::once(src.len()))
96− .collect();
97 let mut i = 0usize;
87 let mut i = 0usize;
98 let mut line = 1usize;
88 let mut line = 1usize;
99 let mut col = 1usize; // column of next char
89 let mut col = 1usize; // column of next char
@@ -107,15 +97,13 @@ pub fn lex(src: &str) -> Result<Vec<SpTok>, LexError> {
107 };
97 };
108 }
98 }
109
99
110− // push helper; `$s..$e` are the token's char indices
100+ // push helper
111 macro_rules! push {
101 macro_rules! push {
112− ($t:expr, $l:expr, $c:expr, $s:expr, $e:expr) => {
102+ ($t:expr, $l:expr, $c:expr) => {
113 toks.push(SpTok {
103 toks.push(SpTok {
114 tok: $t,
104 tok: $t,
115 line: $l,
105 line: $l,
116 col: $c,
106 col: $c,
117− start: bytes[$s],
118− end: bytes[$e],
119 })
107 })
120 };
108 };
121 }
109 }
@@ -123,7 +111,7 @@ pub fn lex(src: &str) -> Result<Vec<SpTok>, LexError> {
123 while i < chars.len() {
111 while i < chars.len() {
124 let c = chars[i];
112 let c = chars[i];
125 if c == '\n' {
113 if c == '\n' {
126− push!(Tok::Newline, line, col, i, i + 1);
114+ push!(Tok::Newline, line, col);
127 i += 1;
115 i += 1;
128 line += 1;
116 line += 1;
129 col = 1;
117 col = 1;
@@ -136,7 +124,6 @@ pub fn lex(src: &str) -> Result<Vec<SpTok>, LexError> {
136 }
124 }
137 let start_line = line;
125 let start_line = line;
138 let start_col = col;
126 let start_col = col;
139− let start = i;
140 // comments
127 // comments
141 if c == '-' && i + 1 < chars.len() && chars[i + 1] == '-' {
128 if c == '-' && i + 1 < chars.len() && chars[i + 1] == '-' {
142 // could be `--` comment, but `->`? no: `->` is dash-gt, distinct.
129 // could be `--` comment, but `->`? no: `->` is dash-gt, distinct.
@@ -160,7 +147,7 @@ pub fn lex(src: &str) -> Result<Vec<SpTok>, LexError> {
160 i += 2;
147 i += 2;
161 col += 2;
148 col += 2;
162 } else if chars[i] == '\n' {
149 } else if chars[i] == '\n' {
163− push!(Tok::Newline, line, col, i, i + 1);
150+ push!(Tok::Newline, line, col);
164 i += 1;
151 i += 1;
165 line += 1;
152 line += 1;
166 col = 1;
153 col = 1;
@@ -172,14 +159,14 @@ pub fn lex(src: &str) -> Result<Vec<SpTok>, LexError> {
172 if depth > 0 {
159 if depth > 0 {
173 err!("unterminated block comment");
160 err!("unterminated block comment");
174 }
161 }
175− // a comment is whitespace (§3.1): one spanning lines has already
162+ // a comment spanning lines must not leave later tokens looking
176− // ended the line it started on, with the Newlines pushed above,
163+ // like continuations of the line it started on
177− // and one that does not must leave its line unbroken
164+ push!(Tok::Newline, line, col);
178 continue;
165 continue;
179 }
166 }
180 // arrow `->` (check before operator `-`)
167 // arrow `->` (check before operator `-`)
181 if c == '-' && i + 1 < chars.len() && chars[i + 1] == '>' {
168 if c == '-' && i + 1 < chars.len() && chars[i + 1] == '>' {
182− push!(Tok::Arrow, start_line, start_col, i, i + 2);
169+ push!(Tok::Arrow, start_line, start_col);
183 i += 2;
170 i += 2;
184 col += 2;
171 col += 2;
185 continue;
172 continue;
@@ -215,7 +202,7 @@ pub fn lex(src: &str) -> Result<Vec<SpTok>, LexError> {
215 if !comp.is_empty() {
202 if !comp.is_empty() {
216 comps.push(comp);
203 comps.push(comp);
217 }
204 }
218− push!(Tok::PathLit(comps), start_line, start_col, start, i);
205+ push!(Tok::PathLit(comps), start_line, start_col);
219 continue;
206 continue;
220 }
207 }
221 // selector: `.` immediately followed by identifier-start
208 // selector: `.` immediately followed by identifier-start
@@ -228,13 +215,13 @@ pub fn lex(src: &str) -> Result<Vec<SpTok>, LexError> {
228 i += 1;
215 i += 1;
229 col += 1;
216 col += 1;
230 }
217 }
231− push!(Tok::Selector(name), start_line, start_col, start, i);
218+ push!(Tok::Selector(name), start_line, start_col);
232 continue;
219 continue;
233 }
220 }
234 // `@` id literals
221 // `@` id literals
235 if c == '@' {
222 if c == '@' {
236 if i + 1 >= chars.len() || !is_ident_char(chars[i + 1]) {
223 if i + 1 >= chars.len() || !is_ident_char(chars[i + 1]) {
237− push!(Tok::NewId, start_line, start_col, i, i + 1);
224+ push!(Tok::NewId, start_line, start_col);
238 i += 1;
225 i += 1;
239 col += 1;
226 col += 1;
240 continue;
227 continue;
@@ -252,7 +239,7 @@ pub fn lex(src: &str) -> Result<Vec<SpTok>, LexError> {
252 if i < chars.len() && is_ident_char(chars[i]) {
239 if i < chars.len() && is_ident_char(chars[i]) {
253 err!("invalid id literal");
240 err!("invalid id literal");
254 }
241 }
255− push!(Tok::IdLit(s), start_line, start_col, start, i);
242+ push!(Tok::IdLit(s), start_line, start_col);
256 continue;
243 continue;
257 }
244 }
258 err!("`@` followed by an identifier character outside k-z is not a valid id literal");
245 err!("`@` followed by an identifier character outside k-z is not a valid id literal");
@@ -275,7 +262,7 @@ pub fn lex(src: &str) -> Result<Vec<SpTok>, LexError> {
275 if !valid_label(&s) {
262 if !valid_label(&s) {
276 err!("invalid label literal");
263 err!("invalid label literal");
277 }
264 }
278− push!(Tok::LabelLit(s), start_line, start_col, start, i);
265+ push!(Tok::LabelLit(s), start_line, start_col);
279 continue;
266 continue;
280 }
267 }
281 // text literal, double- or single-quoted
268 // text literal, double- or single-quoted
@@ -321,7 +308,7 @@ pub fn lex(src: &str) -> Result<Vec<SpTok>, LexError> {
321 }
308 }
322 }
309 }
323 }
310 }
324− push!(Tok::Text(s), start_line, start_col, start, i);
311+ push!(Tok::Text(s), start_line, start_col);
325 continue;
312 continue;
326 }
313 }
327 // integer
314 // integer
@@ -332,7 +319,7 @@ pub fn lex(src: &str) -> Result<Vec<SpTok>, LexError> {
332 i += 1;
319 i += 1;
333 col += 1;
320 col += 1;
334 }
321 }
335− push!(Tok::Int(s.parse::<BigInt>().unwrap()), start_line, start_col, start, i);
322+ push!(Tok::Int(s.parse::<BigInt>().unwrap()), start_line, start_col);
336 continue;
323 continue;
337 }
324 }
338 // identifier / keyword
325 // identifier / keyword
@@ -361,7 +348,7 @@ pub fn lex(src: &str) -> Result<Vec<SpTok>, LexError> {
361 }
348 }
362 _ => Tok::Ident(s),
349 _ => Tok::Ident(s),
363 };
350 };
364− push!(t, start_line, start_col, start, i);
351+ push!(t, start_line, start_col);
365 continue;
352 continue;
366 }
353 }
367 // type name
354 // type name
@@ -377,7 +364,7 @@ pub fn lex(src: &str) -> Result<Vec<SpTok>, LexError> {
377 break;
364 break;
378 }
365 }
379 }
366 }
380− push!(Tok::TypeName(s), start_line, start_col, start, i);
367+ push!(Tok::TypeName(s), start_line, start_col);
381 continue;
368 continue;
382 }
369 }
383 // symbolic operators: longest match
370 // symbolic operators: longest match
@@ -400,7 +387,7 @@ pub fn lex(src: &str) -> Result<Vec<SpTok>, LexError> {
400 }
387 }
401 }
388 }
402 if let Some(op) = matched {
389 if let Some(op) = matched {
403− push!(Tok::Op(op), start_line, start_col, i, i + op.len());
390+ push!(Tok::Op(op), start_line, start_col);
404 i += op.len();
391 i += op.len();
405 col += op.len();
392 col += op.len();
406 continue;
393 continue;
@@ -420,11 +407,11 @@ pub fn lex(src: &str) -> Result<Vec<SpTok>, LexError> {
420 '=' => Tok::Equals,
407 '=' => Tok::Equals,
421 _ => err!(format!("unexpected character `{}`", c)),
408 _ => err!(format!("unexpected character `{}`", c)),
422 };
409 };
423− push!(t, start_line, start_col, i, i + 1);
410+ push!(t, start_line, start_col);
424 i += 1;
411 i += 1;
425 col += 1;
412 col += 1;
426 }
413 }
427− push!(Tok::Eof, line, col, i, i);
414+ push!(Tok::Eof, line, col);
428 Ok(toks)
415 Ok(toks)
429}
416}
430
417
src/lib.rs−14
@@ -1,28 +1,14 @@
1−/// `eprintln!` for `j`'s own messages (§1.4). A stderr that cannot take the
2−/// line (its reader has exited, as in `j … 2>&1 | head`, or the device is
3−/// full) loses it; `eprintln!` would panic instead, and the run would end
4−/// with the runtime's status 101 rather than the one it reports.
5−#[macro_export]
6−macro_rules! report {
7− ($($arg:tt)*) => {{
8− $crate::terminal::report(format_args!($($arg)*));
9− }};
10−}
11−
12pub mod ast;
1pub mod ast;
13pub mod builtins;
2pub mod builtins;
14pub mod config;
3pub mod config;
15−pub mod diagnostics;
16pub mod domain;
4pub mod domain;
17pub mod eval;
5pub mod eval;
18pub mod lex;
6pub mod lex;
19−mod lookup;
20pub mod parse;
7pub mod parse;
21pub mod render;
8pub mod render;
22pub mod repo;
9pub mod repo;
23pub mod shape;
10pub mod shape;
24pub mod show;
11pub mod show;
25−pub mod terminal;
26pub mod value;
12pub mod value;
27
13
28pub mod jj;
14pub mod jj;
src/lookup.rsdeleted−78
@@ -1,78 +0,0 @@
1−//! Indexed snapshot lookups used by the reference configuration.
2−
3−use crate::value::{value_eq, BlobVal, Crash, ListVal, Value};
4−use std::cell::RefCell;
5−use std::collections::{HashMap, VecDeque};
6−use std::rc::Weak;
7−
8−struct SnapshotIndex {
9− identity: (*const Vec<Value>, usize),
10− storage: Weak<Vec<Value>>,
11− // None means a non-path was encountered: use structural equality, as
12− // the language's entryAt does, including errors in later entries.
13− paths: Option<HashMap<Vec<String>, usize>>,
14−}
15−
16−thread_local! {
17− // `diffs` alternates the before/after snapshots. Weak keys and integer
18− // positions keep neither snapshot nor its blobs alive after the caller.
19− static SNAPSHOTS: RefCell<VecDeque<SnapshotIndex>> = const { RefCell::new(VecDeque::new()) };
20−}
21−
22−fn path_key(path: &Value) -> Option<Vec<String>> {
23− path.as_list().ok()?.iter()
24− .map(|v| v.as_text().ok().map(str::to_owned))
25− .collect()
26−}
27−
28−fn indexed_entry(path: &[String], entries: &ListVal) -> Option<Option<usize>> {
29− SNAPSHOTS.with(|cache| {
30− let mut cache = cache.borrow_mut();
31− let identity = entries.identity();
32− if let Some(pos) = cache.iter().position(|c| {
33− c.identity == identity && c.storage.strong_count() > 0
34− }) {
35− let entry = cache.remove(pos).unwrap();
36− cache.push_front(entry);
37− } else {
38− let paths = (|| {
39− let mut paths = HashMap::new();
40− for (i, entry) in entries.iter().enumerate() {
41− let path = path_key(&entry.field("path").ok()?)?;
42− paths.entry(path).or_insert(i);
43− }
44− Some(paths)
45− })();
46− cache.push_front(SnapshotIndex {
47− identity,
48− storage: entries.weak_storage(),
49− paths,
50− });
51− cache.truncate(2);
52− }
53− cache.front().unwrap().paths.as_ref()
54− .map(|paths| paths.get(path).copied())
55− })
56−}
57−
58−/// `(head (filter (\e -> e.path == path) files)).content or blob ""`.
59−/// Index ordinary paths; preserve the full filter's errors and first-match
60−/// behavior for arbitrary values the shallow Snapshot contract also admits.
61−pub(crate) fn content_at(path: &Value, files: &Value) -> Result<Value, Crash> {
62− let lookup = || -> Result<Value, Crash> {
63− if let (Some(key), Value::List(entries)) = (path_key(path), files) {
64− if let Some(found) = indexed_entry(&key, entries) {
65− return found.ok_or_else(|| Crash::new("head: empty list"))
66− .and_then(|i| entries[i].field("content"));
67− }
68− }
69− let mut first = None;
70− for entry in files.as_list()? {
71− if value_eq(&entry.field("path")?, path)? && first.is_none() {
72− first = Some(entry);
73− }
74− }
75− first.ok_or_else(|| Crash::new("head: empty list"))?.field("content")
76− };
77− Ok(lookup().unwrap_or_else(|_| BlobVal::text_blob("")))
78−}
src/main.rs+141−465
@@ -1,10 +1,7 @@
1//! j — a functional-programming-centered CLI for jj repositories (§1).
1//! j — a functional-programming-centered CLI for jj repositories (§1).
2
2
3−mod help;
4−mod setup;
5−
6use j::config::{self, Config};
3use j::config::{self, Config};
7−use j::diagnostics;
4+use j::eval::Interp;
8use j::jj::JjBackend;
5use j::jj::JjBackend;
9use j::parse::parse_expr;
6use j::parse::parse_expr;
10use j::value::{Crash, Value};
7use j::value::{Crash, Value};
@@ -13,47 +10,20 @@ use std::io::{IsTerminal, Read};
13use std::process::ExitCode;
10use std::process::ExitCode;
14use std::rc::Rc;
11use std::rc::Rc;
15
12
16−const USAGE: &str = "usage: j EXPRESSION… (or echo EXPRESSION | j)
13+const USAGE: &str = "usage: j EXPRESSION… (or echo EXPRESSION | j)";
17−
18−Configure identity: j setup
19−Start a repository: j init or j clone URL
20−Inspect your work: j status or j tree
21−Discover commands: j help or j commands
22−Command details: j help NAME";
23
14
24fn err(status: u8, msg: impl std::fmt::Display) -> ExitCode {
15fn err(status: u8, msg: impl std::fmt::Display) -> ExitCode {
25− j::terminal::report_error(format_args!("j: {}", msg));
16+ eprintln!("j: {}", msg);
26 ExitCode::from(status)
17 ExitCode::from(status)
27}
18}
28
19
29−/// Report a failure that ends the process, keeping the status as a number so
30−/// callers do not have to recover it from an `ExitCode`.
31−fn fail(status: u8, msg: impl std::fmt::Display) -> u8 {
32− j::terminal::report_error(format_args!("j: {}", msg));
33− status
34−}
35−
36−/// Write a run's output to stdout (§1.2.9, `ops`). A reader that stops early
37−/// (`j tree | head`) closes the pipe; that is not a failure (§1.4), so the
38−/// rest is dropped and the run still succeeds. Any other write failure is an
39−/// error line. `print!` would panic on either.
40−fn print_output(s: &str) -> ExitCode {
41− let mut out = std::io::stdout().lock();
42− match j::terminal::write_output(&mut out, s) {
43− Ok(()) => ExitCode::SUCCESS,
44− Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => ExitCode::SUCCESS,
45− Err(e) => err(1, format!("cannot write output: {}", e)),
46− }
47−}
48−
49−fn crash_err(c: &Crash, expr_text: &str, cfg: &Config) -> ExitCode {
20+fn crash_err(c: &Crash, expr_text: Option<&str>) -> ExitCode {
50− j::terminal::report_error(format_args!("j: crash: {}", c.msg));
21+ eprintln!("j: crash: {}", c.msg);
51− match &c.def {
22+ match (&c.def, expr_text) {
52− Some(d) => j::report!(" in {}, from {}", d, expr_text),
23+ (Some(d), Some(e)) => eprintln!(" in {}, from {}", d, e),
53− None => j::report!(" from {}", expr_text),
24+ (None, Some(e)) => eprintln!(" from {}", e),
54− }
25+ (Some(d), None) => eprintln!(" in {}", d),
55− if let Some(hint) = diagnostics::expression_hint(expr_text, cfg, c) {
26+ (None, None) => {}
56− j::report!(" {}", hint);
57 }
27 }
58 ExitCode::from(1)
28 ExitCode::from(1)
59}
29}
@@ -72,371 +42,184 @@ fn config_path() -> String {
72/// installed (the share/j/config.j file may not exist next to the binary).
42/// installed (the share/j/config.j file may not exist next to the binary).
73const DEFAULT_CONFIG: &str = include_str!("../config.j");
43const DEFAULT_CONFIG: &str = include_str!("../config.j");
74
44
75−fn load_config_file() -> Result<(Config, String), u8> {
45+fn load_config_file() -> Result<(Config, String), ExitCode> {
76 let path = config_path();
46 let path = config_path();
77 let src = match std::fs::read_to_string(&path) {
47 let src = match std::fs::read_to_string(&path) {
78 Ok(s) => s,
48 Ok(s) => s,
79− Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
49+ Err(_) => {
80 // No user config yet: materialise the default there, editable by
50 // No user config yet: materialise the default there, editable by
81 // the current user, so they have a starting point to customise.
51 // the current user, so they have a starting point to customise.
82− match write_default_config(&path) {
52+ if let Err(e) = write_default_config(&path) {
83− Ok(()) => j::report!("j: created an editable default config at {}", path),
84− // Another invocation may have created it after our read.
85− // Read that file below, without overwriting it.
86− Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
87− Err(e) => return Err(fail(3, format!("cannot create config at {}: {}", path, e))),
53+ return Err(err(3, format!("cannot create config at {}: {}", path, e)));
88 }
54 }
55+ eprintln!("j: created an editable default config at {}", path);
89 match std::fs::read_to_string(&path) {
56 match std::fs::read_to_string(&path) {
90 Ok(s) => s,
57 Ok(s) => s,
91− Err(e) => return Err(fail(3, format!("cannot read config at {}: {}", path, e))),
58+ Err(e) => return Err(err(3, format!("cannot read config at {}: {}", path, e))),
92 }
59 }
93 }
60 }
94− Err(e) => return Err(fail(3, format!("cannot read config at {}: {}", path, e))),
95 };
61 };
96 match config::load_config(&src) {
62 match config::load_config(&src) {
97 Ok(c) => Ok((c, src)),
63 Ok(c) => Ok((c, src)),
98− Err(e) => {
64+ Err(e) => Err(err(3, e.message())),
99− let message = match e {
100− config::ConfigError::Parse(p) => {
101− diagnostics::format_source_error(&path, &src, p.line, &p.msg)
102− }
103− config::ConfigError::Validation(message) => format!("{}: {}", path, message),
104− };
105− Err(fail(3, format!("{}\n Edit this file to repair your config; see `j help config`.", message)))
106− }
107 }
65 }
108}
66}
109
67
110/// Write the embedded default config to `path`, creating parent directories
68/// Write the embedded default config to `path`, creating parent directories
111/// and making the file readable and writable by the current user.
69/// and making the file readable and writable by the current user.
112fn write_default_config(path: &str) -> std::io::Result<()> {
70fn write_default_config(path: &str) -> std::io::Result<()> {
113− use std::io::Write;
114 if let Some(parent) = std::path::Path::new(path).parent() {
71 if let Some(parent) = std::path::Path::new(path).parent() {
115 std::fs::create_dir_all(parent)?;
72 std::fs::create_dir_all(parent)?;
116 }
73 }
117− let mut options = std::fs::OpenOptions::new();
74+ std::fs::write(path, DEFAULT_CONFIG)?;
118− options.write(true).create_new(true);
119 // user-read+write (0o600 on unix): the file is meant to be edited by them
75 // user-read+write (0o600 on unix): the file is meant to be edited by them
120 #[cfg(unix)]
76 #[cfg(unix)]
121 {
77 {
122− use std::os::unix::fs::OpenOptionsExt;
78+ use std::os::unix::fs::PermissionsExt;
123− options.mode(0o600);
79+ std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
124− }
125− options.open(path)?.write_all(DEFAULT_CONFIG.as_bytes())
126−}
127−
128−const RESERVED: &[&str] = &[
129− "init", "clone", "remote", "fetch", "push", "undo", "redo", "ops", "help", "commands", "setup",
130−];
131−
132−/// Discovery reads the user's vocabulary without creating or evaluating it.
133−/// A missing file uses the embedded reference; other read errors stay visible.
134−fn read_help_config() -> Result<(String, String), u8> {
135− let path = config_path();
136− match std::fs::read_to_string(&path) {
137− Ok(src) => Ok((src, path)),
138− Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok((
139− DEFAULT_CONFIG.to_string(),
140− format!("embedded default config.j (no user config at {})", path),
141− )),
142− Err(e) => Err(fail(3, format!("cannot read config at {}: {}", path, e))),
143 }
80 }
81+ Ok(())
144}
82}
145
83
146−fn run_help(words: &[&str]) -> ExitCode {
84+const RESERVED: &[&str] = &["init", "clone", "remote", "fetch", "push", "undo", "redo", "ops"];
147− match words {
148− [_] => print_output(help::OVERVIEW),
149− [_, name] => {
150− if *name == "config" {
151− return print_output(&format!("{}\nConfig: {}\n", help::CONFIG_HELP, config_path()));
152− }
153− if let Some(output) = help::reserved_help(name) {
154− return print_output(&output);
155− }
156− let (src, path) = or_exit(read_help_config());
157− match help::command_help(&src, name) {
158− Ok(Some(output)) => print_output(&format!("{}\nConfig: {}\n", output, path)),
159− Ok(None) => err(
160− 2,
161− format!("unknown help topic `{}`; run `j commands` to browse or `j commands {}` to search", name, name),
162− ),
163− Err(message) => err(3, format!("{}: {}", path, message)),
164− }
165− }
166− _ => err(
167− 2,
168− "usage: j help [NAME]; run `j commands` to search available names",
169− ),
170− }
171−}
172−
173−fn run_commands(text: &str) -> ExitCode {
174− let query = text.strip_prefix("commands").unwrap_or("").trim();
175− let (src, path) = or_exit(read_help_config());
176− match help::command_list(&src, query) {
177− Ok(output) => print_output(&format!("{}\nConfig: {}\n", output, path)),
178− Err(message) => err(3, format!("{}: {}", path, message)),
179− }
180−}
181−
182−fn command_usage(usage: &str, command: &str) -> ExitCode {
183− err(
184− 2,
185− format!("usage: j {}; see `j help {}` for examples", usage, command),
186− )
187−}
188−
189−fn command_result(result: Result<(), (u8, String)>, message: &str) -> ExitCode {
190− match result {
191− Ok(()) => {
192− j::terminal::success(message);
193− ExitCode::SUCCESS
194− }
195− Err(e) => err(e.0, e.1),
196− }
197−}
198−
199−fn setup_identity(cfg: &Config) -> Result<String, u8> {
200− j::jj::configured_identity(cfg).map_err(|(_, message)| {
201− fail(3, format!("{}: {}\n Edit `user` in this file; see `j help config`.", config_path(), message.trim_start_matches("config.j: ")))
202− })
203−}
204−
205−fn operation_result(result: Result<String, (u8, String)>) -> ExitCode {
206− match result {
207− Ok(message) => {
208− j::terminal::success(&message);
209− ExitCode::SUCCESS
210− }
211− Err((code, message)) => err(code, message),
212− }
213−}
214
85
215fn main() -> ExitCode {
86fn main() -> ExitCode {
216 // evaluation builds deep continuation chains (trampolined, but the final
87 // evaluation builds deep continuation chains (trampolined, but the final
217 // continuation still tears down by recursive Drop), so run the real work
88 // continuation still tears down by recursive Drop), so run the real work
218 // on a thread with a large stack to keep very deep histories safe
89 // on a thread with a large stack to keep very deep histories safe
219− let code = std::thread::Builder::new()
90+ std::thread::Builder::new()
220 .stack_size(512 * 1024 * 1024)
91 .stack_size(512 * 1024 * 1024)
221 .spawn(run)
92 .spawn(run)
222 .expect("failed to spawn worker thread")
93 .expect("failed to spawn worker thread")
223 .join()
94 .join()
224− .expect("worker thread panicked");
95+ .expect("worker thread panicked")
225− // §1.3: a signal to stop that came while the working copy was checked
226− // out takes effect now, once the run has said what it had to
227− j::jj::raise_deferred();
228− code
229−}
230−
231−/// True when stdin has input (or end-of-input) waiting within `ms`
232−/// milliseconds. A closed or exhausted stdin reports ready at once, so the
233−/// wait is only paid for a pipe that is open but idle.
234−fn stdin_is_ready(ms: i32) -> bool {
235− let mut fds = libc::pollfd {
236− fd: libc::STDIN_FILENO,
237− events: libc::POLLIN,
238− revents: 0,
239− };
240− unsafe { libc::poll(&mut fds, 1, ms) > 0 }
241}
96}
242
97
243fn run() -> ExitCode {
98fn run() -> ExitCode {
244− // §1: an expression is text, so an argument that is not UTF-8 is a
245− // usage error (`std::env::args` would panic on it)
246− let args: Vec<String> = match std::env::args_os()
99+ let args: Vec<String> = std::env::args().skip(1).collect();
247− .skip(1)
248− .map(|a| a.into_string())
249− .collect()
250− {
251− Ok(args) => args,
252− Err(a) => return err(2, format!("argument is not valid UTF-8: {:?}", a)),
253− };
254− // §1: stdin vs arguments. With no arguments the expression comes from
100+ // §1: stdin vs arguments
255− // stdin, so reading to end-of-input is the whole point. With arguments,
256− // stdin is only read to report the "both" error — and reading it blindly
257− // hangs whenever the process inherited a pipe nobody is writing to (a
258− // shell loop, a CI step), so wait only briefly for input to appear.
259− // One byte already proves the "both" error: do not wait for the pipe's
260− // writer to close it. Bytes that are not UTF-8 are input too, and an
261− // error only once they are the expression.
262− let mut stdin_content = Vec::new();
101+ let mut stdin_content = String::new();
263 let stdin_has_data = {
102 let stdin_has_data = {
264 if std::io::stdin().is_terminal() {
103 if std::io::stdin().is_terminal() {
265 false
104 false
266− } else if args.is_empty() || stdin_is_ready(50) {
105+ } else {
267− let limit = if args.is_empty() { u64::MAX } else { 1 };
268− match std::io::stdin().take(limit).read_to_end(&mut stdin_content) {
106+ match std::io::stdin().read_to_string(&mut stdin_content) {
269 Ok(n) => n > 0,
107 Ok(n) => n > 0,
270 Err(_) => false,
108 Err(_) => false,
271 }
109 }
272− } else {
273− false
274 }
110 }
275 };
111 };
276 if stdin_has_data && !args.is_empty() {
112 if stdin_has_data && !args.is_empty() {
277 return err(2, "expression given both as arguments and on stdin");
113 return err(2, "expression given both as arguments and on stdin");
278 }
114 }
279− // An argv help flag is a request for help, never an expression comment.
280− // Comments embedded in a single expression argument or stdin remain part
281− // of the language, including `j 'new --help'`.
282− if args.iter().skip(1).any(|arg| matches!(arg.as_str(), "--help" | "-h")) {
283− let topic = args.first().and_then(|arg| arg.split_whitespace().next());
284− return match topic {
285− Some("--help" | "-h" | "help") | None => run_help(&["help"]),
286− Some(name) => run_help(&["help", name]),
287− };
288− }
289 let text = if stdin_has_data {
115 let text = if stdin_has_data {
290− match String::from_utf8(stdin_content) {
116+ stdin_content
291− Ok(text) => text,
292− Err(_) => return err(2, "the expression on stdin is not valid UTF-8"),
293− }
294 } else if !args.is_empty() {
117 } else if !args.is_empty() {
295 args.join(" ")
118 args.join(" ")
296 } else {
119 } else {
297− j::report!("{}", USAGE);
120+ eprintln!("{}", USAGE);
298 return ExitCode::from(2);
121 return ExitCode::from(2);
299 };
122 };
300
123
301 // §1.1: reserved commands
124 // §1.1: reserved commands
302 let trimmed = text.trim();
125 let trimmed = text.trim();
303− if trimmed.is_empty() {
304− j::report!("{}", USAGE);
305− return ExitCode::from(2);
306− }
307 let first = trimmed.split_whitespace().next().unwrap_or("");
126 let first = trimmed.split_whitespace().next().unwrap_or("");
308− // Preserve shell argument boundaries for literal reserved-command
309− // arguments. Single quoted commands and stdin still use text words.
310− let words: Vec<&str> = if args.first().is_some_and(|arg| arg == first) {
311− args.iter().map(String::as_str).collect()
312− } else {
313− trimmed.split_whitespace().collect()
314− };
315− if matches!(first, "--help" | "-h") {
316− return run_help(&words);
317− }
318 if RESERVED.contains(&first) {
127 if RESERVED.contains(&first) {
319− return run_reserved(first, trimmed, &words);
128+ return run_reserved(first, trimmed);
320 }
129 }
321
130
322 run_expression(trimmed, true)
131 run_expression(trimmed, true)
323}
132}
324
133
325−fn run_reserved(cmd: &str, text: &str, words: &[&str]) -> ExitCode {
134+fn run_reserved(cmd: &str, text: &str) -> ExitCode {
135+ let words: Vec<&str> = text.split_whitespace().collect();
326 match cmd {
136 match cmd {
327− "help" => run_help(words),
328− "commands" => run_commands(text),
329− "setup" => match setup::run(std::path::Path::new(&config_path()), DEFAULT_CONFIG, &words[1..]) {
330− Ok(output) => print_output(&output),
331− Err((code, message)) => err(code, message),
332− },
333 "init" => {
137 "init" => {
334 if words.len() != 1 {
138 if words.len() != 1 {
335− return command_usage("init", "init");
139+ return err(2, "usage: j init");
336 }
140 }
337 let (cfg, _) = or_exit(load_config_file());
141 let (cfg, _) = or_exit(load_config_file());
338− let identity = or_exit(setup_identity(&cfg));
142+ match j::jj::cmd_init(&cfg) {
339− command_result(
143+ Ok(()) => ExitCode::SUCCESS,
340− j::terminal::with_progress("Creating repository", || j::jj::cmd_init(&cfg)),
144+ Err(e) => err(e.0, e.1),
341− &format!("Repository created; identity {}", identity),
145+ }
342− )
343 }
146 }
344 "clone" => {
147 "clone" => {
345 if words.len() != 2 && words.len() != 3 {
148 if words.len() != 2 && words.len() != 3 {
346− return command_usage("clone URL [DIR]", "clone");
149+ return err(2, "usage: j clone URL [DIR]");
347 }
150 }
151+ let (cfg, _) = or_exit(load_config_file());
348 let dir = if words.len() == 3 {
152 let dir = if words.len() == 3 {
349 words[2].to_string()
153 words[2].to_string()
350 } else {
154 } else {
351− match default_clone_dir(words[1]) {
155+ default_clone_dir(words[1])
352− Some(dir) => dir,
353− None => {
354− let msg = format!(
355− "`{}` names no directory to clone into; give one: j clone URL DIR",
356− words[1]
357− );
358− return err(2, msg);
359− }
360− }
361 };
156 };
362− let (cfg, _) = or_exit(load_config_file());
363− let identity = or_exit(setup_identity(&cfg));
364− command_result(
365− j::terminal::with_progress("Cloning repository", || j::jj::cmd_clone(&cfg, words[1], &dir)),
157+ match j::jj::cmd_clone(&cfg, words[1], &dir) {
366− &format!("Repository cloned into {}; identity {}\n Next: cd {} && j status", dir, identity, shell_word(&dir)),
158+ Ok(()) => ExitCode::SUCCESS,
367− )
159+ Err(e) => err(e.0, e.1),
160+ }
368 }
161 }
369 "remote" => {
162 "remote" => {
370 if words.len() != 2 {
163 if words.len() != 2 {
371− return command_usage("remote URL", "remote");
164+ return err(2, "usage: j remote URL");
372 }
165 }
373 let backend = or_exit(open_repo());
166 let backend = or_exit(open_repo());
374− command_result(
375− j::terminal::with_progress("Updating remote", || backend.cmd_remote(words[1])),
167+ match backend.cmd_remote(words[1]) {
376− "Remote updated",
168+ Ok(()) => ExitCode::SUCCESS,
377− )
169+ Err(e) => err(e.0, e.1),
170+ }
378 }
171 }
379 "fetch" => {
172 "fetch" => {
380 if words.len() != 1 {
173 if words.len() != 1 {
381− return command_usage("fetch", "fetch");
174+ return err(2, "usage: j fetch");
175+ }
176+ let (cfg, _) = or_exit(load_config_file());
177+ let backend = or_exit(open_repo_locked(&cfg));
178+ match backend.cmd_fetch() {
179+ Ok(()) => ExitCode::SUCCESS,
180+ Err(e) => err(e.0, e.1),
382 }
181 }
383− let backend = or_exit(open_repo_noconfig());
384− command_result(
385− j::terminal::with_progress("Fetching changes", || backend.cmd_fetch()),
386− "Fetch complete",
387− )
388 }
182 }
389 "push" => {
183 "push" => {
390 let rest = text.trim_start_matches("push").trim();
184 let rest = text.trim_start_matches("push").trim();
391 if rest.is_empty() {
185 if rest.is_empty() {
392− return command_usage("push EXPR", "push");
186+ return err(2, "usage: j push EXPR");
393 }
187 }
394− let (mut cfg, _) = or_exit(load_config_file());
188+ let (cfg, _) = or_exit(load_config_file());
395 let backend = or_exit(open_repo_locked(&cfg));
189 let backend = or_exit(open_repo_locked(&cfg));
396 // evaluate EXPR against the recorded repository (no snapshot)
190 // evaluate EXPR against the recorded repository (no snapshot)
397− match j::terminal::with_progress("Pushing changes", || backend.cmd_push(&mut cfg, rest)) {
191+ match backend.cmd_push(&cfg, rest) {
398− Err((3, message)) if message.starts_with("config.j:") => {
399− err(3, format!("{}: {}\n See `j help config` for configuration help.", config_path(), message.trim_start_matches("config.j: ")))
400− }
401− Ok(outcome) => {
402− if outcome.published + outcome.deleted + outcome.unchanged == 0 {
403− j::report!("j: No bookmarks selected; nothing published.\n First publication: j 'push (label \"feature\" here)'\n Inspect available commits with `j tree` and check your selector.");
404− } else {
405− j::terminal::success(&format!(
406− "Push complete: {} published, {} deleted, {} unchanged bookmark(s)",
407− outcome.published, outcome.deleted, outcome.unchanged,
408− ));
409− }
410− ExitCode::SUCCESS
192+ Ok(()) => ExitCode::SUCCESS,
411− }
193+ Err(e) => err(e.0, e.1),
412− Err((code, message)) => err(code, message),
413 }
194 }
414 }
195 }
415 "undo" => {
196 "undo" => {
416 if words.len() != 1 {
197 if words.len() != 1 {
417− return command_usage("undo", "undo");
198+ return err(2, "usage: j undo");
418 }
199 }
419 let backend = or_exit(open_repo_noconfig());
200 let backend = or_exit(open_repo_noconfig());
420− operation_result(
421− j::terminal::with_progress("Undoing operation", || backend.cmd_undo(false)),
201+ match backend.cmd_undo(false) {
422− )
202+ Ok(()) => ExitCode::SUCCESS,
203+ Err(e) => err(e.0, e.1),
204+ }
423 }
205 }
424 "redo" => {
206 "redo" => {
425 if words.len() != 1 {
207 if words.len() != 1 {
426− return command_usage("redo", "redo");
208+ return err(2, "usage: j redo");
427 }
209 }
428 let backend = or_exit(open_repo_noconfig());
210 let backend = or_exit(open_repo_noconfig());
429− operation_result(
430− j::terminal::with_progress("Redoing operation", || backend.cmd_undo(true)),
211+ match backend.cmd_undo(true) {
431− )
212+ Ok(()) => ExitCode::SUCCESS,
213+ Err(e) => err(e.0, e.1),
214+ }
432 }
215 }
433 "ops" => {
216 "ops" => {
434 if words.len() != 1 {
217 if words.len() != 1 {
435− return command_usage("ops", "ops");
218+ return err(2, "usage: j ops");
436 }
219 }
437 let backend = or_exit(open_repo_noconfig());
220 let backend = or_exit(open_repo_noconfig());
438− match j::terminal::with_progress("Loading operation log", || backend.cmd_ops()) {
221+ match backend.cmd_ops() {
439− Ok(log) => print_output(&log),
222+ Ok(()) => ExitCode::SUCCESS,
440 Err(e) => err(e.0, e.1),
223 Err(e) => err(e.0, e.1),
441 }
224 }
442 }
225 }
@@ -444,101 +227,101 @@ fn run_reserved(cmd: &str, text: &str, words: &[&str]) -> ExitCode {
444 }
227 }
445}
228}
446
229
447−/// `clone`'s default DIR (§1.1): the last component of `url`'s path, a
448−/// final `.git` component skipped, minus `.git`, as `git clone` names it;
449−/// in an scp-style `host:repo.git`, what follows the `:`. None when that
450−/// leaves no name.
451−fn default_clone_dir(url: &str) -> Option<String> {
230+fn default_clone_dir(url: &str) -> String {
452− let mut path = url.trim_end_matches('/');
231+ let last = url.trim_end_matches('/').rsplit('/').next().unwrap_or("repo");
453− if let Some(repo) = path.strip_suffix("/.git") {
454− path = repo.trim_end_matches('/');
455− }
456− let last = match path.rfind('/') {
457− Some(i) => &path[i + 1..],
458− None => path.rsplit(':').next().unwrap_or(path),
459− };
460− let name = last.strip_suffix(".git").unwrap_or(last);
232+ last.strip_suffix(".git").unwrap_or(last).to_string()
461− match name {
462− "" | "." | ".." => None,
463− name => Some(name.to_string()),
464− }
465}
233}
466
234
467−/// A runnable shell word for literal clone destinations, including spaces and
468−/// quotes. Prefix a leading dash so `cd` does not interpret it as an option.
469−fn shell_word(path: &str) -> String {
470− let path = if path.starts_with('-') { format!("./{}", path) } else { path.to_string() };
471− format!("'{}'", path.replace('\'', "'\\''"))
472−}
473−
474−fn or_exit<T>(r: Result<T, u8>) -> T {
235+fn or_exit<T>(r: Result<T, ExitCode>) -> T {
475 match r {
236 match r {
476 Ok(v) => v,
237 Ok(v) => v,
477− Err(code) => std::process::exit(code as i32),
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+ }
478 }
252 }
253+ 1
479}
254}
480
255
481−fn open_repo() -> Result<Rc<JjBackend>, u8> {
256+fn open_repo() -> Result<Rc<JjBackend>, ExitCode> {
482 match JjBackend::open(None) {
257 match JjBackend::open(None) {
483 Ok(b) => Ok(Rc::new(b)),
258 Ok(b) => Ok(Rc::new(b)),
484− Err(e) => Err(fail(e.0, e.1)),
259+ Err(e) => Err(err(e.0, e.1)),
485 }
260 }
486}
261}
487
262
488−fn open_repo_noconfig() -> Result<Rc<JjBackend>, u8> {
263+fn open_repo_noconfig() -> Result<Rc<JjBackend>, ExitCode> {
489 // undo/redo/ops do not read the config (§6.1), but take the lock
264 // undo/redo/ops do not read the config (§6.1), but take the lock
490 match JjBackend::open(None) {
265 match JjBackend::open(None) {
491 Ok(b) => {
266 Ok(b) => {
492 b.take_lock();
267 b.take_lock();
493 Ok(Rc::new(b))
268 Ok(Rc::new(b))
494 }
269 }
495− Err(e) => Err(fail(e.0, e.1)),
270+ Err(e) => Err(err(e.0, e.1)),
496 }
271 }
497}
272}
498
273
499−fn open_repo_locked(cfg: &Config) -> Result<Rc<JjBackend>, u8> {
274+fn open_repo_locked(cfg: &Config) -> Result<Rc<JjBackend>, ExitCode> {
500 match JjBackend::open(Some(cfg)) {
275 match JjBackend::open(Some(cfg)) {
501 Ok(b) => {
276 Ok(b) => {
502 b.take_lock();
277 b.take_lock();
503 Ok(Rc::new(b))
278 Ok(Rc::new(b))
504 }
279 }
505− Err(e) => Err(fail(e.0, e.1)),
280+ Err(e) => Err(err(e.0, e.1)),
506 }
281 }
507}
282}
508
283
509fn run_expression(text: &str, snapshot: bool) -> ExitCode {
284fn run_expression(text: &str, snapshot: bool) -> ExitCode {
510 // §1.2.1: locate the repository
285 // §1.2.1: locate the repository
511− let (mut cfg, _cfg_src) = or_exit(load_config_file());
286+ let (cfg, _cfg_src) = or_exit(load_config_file());
512 let backend = or_exit(open_repo_locked(&cfg));
287 let backend = or_exit(open_repo_locked(&cfg));
513
288
514 // §1.2.2: parse the expression
289 // §1.2.2: parse the expression
515 let outer = Rc::new(cfg.global_names.clone());
290 let outer = Rc::new(cfg.global_names.clone());
516 let mut expr = match parse_expr(text, outer) {
291 let mut expr = match parse_expr(text, outer) {
517 Ok(e) => e,
292 Ok(e) => e,
518− Err(p) => return err(3, diagnostics::format_source_error("expression", text, p.line, &p.msg)),
293+ Err(p) => return err(3, format!("line {}: {}", p.line, p.msg)),
519 };
294 };
520
295
521 // §1.2.4: build the current repository value (with snapshot)
296 // §1.2.4: build the current repository value (with snapshot)
522 let (mut interp, loaded_repo, current_repo) =
297 let (mut interp, loaded_repo, current_repo) =
523− match j::terminal::with_progress("Reading repository", || backend.build_interp(&mut cfg, text, snapshot)) {
298+ match backend.build_interp(&cfg, text, snapshot) {
524 Ok(t) => t,
299 Ok(t) => t,
525 Err((2, m)) => return err(2, m),
300 Err((2, m)) => return err(2, m),
526− Err((3, m)) => return err(3, format!("{}: {}\n See `j help config` for configuration help.", config_path(), m.trim_start_matches("config.j: "))),
527 Err((_, m)) => return err(1, m),
301 Err((_, m)) => return err(1, m),
528 };
302 };
529
303
530− // §1.2.5: the config's Id literals were resolved by build_interp, as soon
304+ // §1.2.5: resolve Id literals in config and expression
531− // as the visible ids were known; the expression's are resolved here
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+ }
532 expr = match config::resolve_ids(&expr, &interp) {
315 expr = match config::resolve_ids(&expr, &interp) {
533 Ok(e) => e,
316 Ok(e) => e,
534 Err(failures) => {
317 Err(failures) => {
535 let (prefix, candidates) = &failures[0];
318 let (prefix, candidates) = &failures[0];
536 if candidates.is_empty() {
319 if candidates.is_empty() {
537− j::terminal::report_error(format_args!("j: crash: `@{}` matches no commit", prefix));
320+ eprintln!("j: crash: `@{}` matches no commit", prefix);
538 } else {
321 } else {
539− j::terminal::report_error(format_args!("j: crash: `@{}` is ambiguous", prefix));
322+ eprintln!("j: crash: `@{}` is ambiguous", prefix);
540 for c in candidates {
323 for c in candidates {
541− j::report!(" {}", c);
324+ eprintln!(" {}", c);
542 }
325 }
543 }
326 }
544 return ExitCode::from(1);
327 return ExitCode::from(1);
@@ -546,140 +329,45 @@ fn run_expression(text: &str, snapshot: bool) -> ExitCode {
546 };
329 };
547
330
548 // §1.2.6: evaluate config definitions, then the expression
331 // §1.2.6: evaluate config definitions, then the expression
549− if let Err(c) = j::terminal::with_progress("Evaluating configuration", || {
550− config::eval_config(&mut interp, &cfg)
332+ if let Err(c) = config::eval_config(&mut interp, &cfg) {
551− }) {
552− return err(3, format!("{}: {}\n See `j help config` for configuration help.", config_path(), c.msg));
333+ return err(3, format!("config.j: {}", c.msg));
553 }
334 }
554 *interp.old_repo.borrow_mut() = Some(loaded_repo.clone());
335 *interp.old_repo.borrow_mut() = Some(loaded_repo.clone());
555− *interp.given_repo.borrow_mut() = Some(current_repo.clone());
556 let env = interp.global_env();
336 let env = interp.global_env();
557 let expr_rc = Rc::new(expr);
337 let expr_rc = Rc::new(expr);
558− let v = match j::terminal::with_progress("Evaluating expression", || {
559− let v = interp.eval(&expr_rc, &env)?;
338+ let mut v = match interp.eval(&expr_rc, &env) {
560− // §1.2.7: apply a function result to the repo, once.
561− if matches!(v, Value::Fun(_)) {
562− interp.apply(v, current_repo.clone())
563− } else {
564− Ok(v)
565− }
566− }) {
567 Ok(v) => v,
339 Ok(v) => v,
568− Err(c) => return crash_err(&c, text, &cfg),
340+ Err(c) => return crash_err(&c, Some(text)),
569 };
341 };
570
342
343+ // §1.2.7: apply a function result to the repo, once
344+ if matches!(v, Value::Fun(_)) {
345+ v = match interp.apply(v, current_repo.clone()) {
346+ Ok(v) => v,
347+ Err(c) => return crash_err(&c, Some(text)),
348+ };
349+ }
350+
571 // §1.2.8: a Repo is persisted (unless unchanged)
351 // §1.2.8: a Repo is persisted (unless unchanged)
572− if is_repo_value(&v) {
352+ if is_repo_value(&interp, &v) {
573− match j::terminal::with_progress("Saving changes", || {
574− backend.persist(&mut interp, &cfg, &loaded_repo, &current_repo, &v, text)
353+ match backend.persist(&mut interp, &cfg, &loaded_repo, &current_repo, &v, text) {
575− }) {
576− Ok(changed) => {
577− if std::io::stdout().is_terminal() && std::io::stderr().is_terminal() {
578− let outcome = if changed { "Saved changes" } else { "No changes" };
579− j::terminal::success(&format!("{}; {}", outcome, focus_summary(&v)));
580− }
581− if changed {
582− report_conflicts(&interp, &loaded_repo, &v);
583− }
584− ExitCode::SUCCESS
354+ Ok(()) => ExitCode::SUCCESS,
585− }
586− Err(c) if c.msg.starts_with("config.j: user identity") => {
587− err(3, format!("{}: {}\n Edit `user` in this file; see `j help config`.", config_path(), c.msg.trim_start_matches("config.j: ")))
588− }
589− Err(c) => crash_err(&c, text, &cfg),
355+ Err(c) => crash_err(&c, Some(text)),
590 }
356 }
591 } else {
357 } else {
592 // §1.2.9: display
358 // §1.2.9: display
593 let color = j::render::color_enabled("auto");
359 let color = j::render::color_enabled("auto");
594− match j::terminal::with_progress("Rendering output", || j::render::display(&mut interp, &v, color)) {
360+ match j::render::display(&mut interp, &v, color) {
595− Ok(s) => print_output(&s),
361+ Ok(s) => {
596− Err(c) => crash_err(&c, text, &cfg),
362+ print!("{}", s);
597− }
363+ ExitCode::SUCCESS
598− }
599−}
600−
601−/// Report newly introduced conflicts and conflicts in the checked-out focus.
602−/// Stored, resolved trees are skipped without loading their entries or blobs.
603−fn conflict_paths(interp: &j::eval::Interp, repo: &Value) -> Result<BTreeSet<(String, String)>, Crash> {
604− let mut paths = BTreeSet::new();
605− for commit in j::repo::all_commits(repo)? {
606− let Value::Id(id) = commit.field("id")? else { continue };
607− let Value::Record(fields) = &commit else { continue };
608− if let Some(Value::Thunk(files)) = fields.get("files") {
609− // `new` can inherit an unchanged snapshot from another commit.
610− // Its stored origin still answers for those files even when the
611− // new commit has a different id.
612− if files.origin().and_then(|origin| interp.backend.has_conflict(origin)) == Some(false) {
613− continue;
614− }
615− }
616− for entry in commit.field("files")?.as_list()? {
617− if matches!(entry.field("content")?, Value::Blob(blob) if blob.is_unresolved()) {
618− let path = entry.field("path")?.as_list()?.iter()
619− .map(|part| part.as_text().map(str::to_owned))
620− .collect::<Result<Vec<_>, _>>()?.join("/");
621− paths.insert((id.to_string(), path));
622 }
364 }
365+ Err(c) => crash_err(&c, Some(text)),
623 }
366 }
624 }
367 }
625− Ok(paths)
626}
368}
627
369
628−fn report_conflicts(interp: &j::eval::Interp, before: &Value, after: &Value) {
370+fn is_repo_value(interp: &Interp, v: &Value) -> bool {
629− let summary = || -> Result<(), Crash> {
630− let old = conflict_paths(interp, before)?;
631− let new = conflict_paths(interp, after)?;
632− let Value::Id(focus) = after.field("root")?.field("id")? else { return Ok(()) };
633− let relevant: Vec<_> = new.iter()
634− .filter(|(id, path)| id == focus.as_str() || !old.contains(&(id.clone(), path.clone())))
635− .collect();
636− if relevant.is_empty() {
637− return Ok(());
638− }
639− j::report!("j: warning: {} unresolved file(s) remain:", relevant.len());
640− for (id, path) in relevant.iter().take(10) {
641− j::report!(" @{} {}", interp.unique_prefix(id), path.escape_debug());
642− }
643− if relevant.len() > 10 {
644− j::report!(" … and {} more; inspect with `j conflicts`.", relevant.len() - 10);
645− }
646− j::report!(" Run `j conflicts` to inspect affected commits. In the focused commit, edit the conflict markers, run `j id` to record the resolution, then `j conflicts` to verify.\n For another commit, first use `j by @ID`. See `j help conflicts`.");
647− Ok(())
648− };
649− // Saving has succeeded: an optional diagnostic must not turn that into a
650− // failed operation that the user might retry.
651− if let Err(c) = summary() {
652− j::report!("j: changes saved, but conflict details could not be read: {}", c.msg);
653− }
654−}
655−
656−/// Summaries inspect only commit metadata, never force file blobs. Use the
657−/// resulting tree's ids, including any commits created by this expression.
658−fn focus_summary(repo: &Value) -> String {
659− let summary = || -> Result<String, Crash> {
660− let focus = repo.field("root")?;
661− let Value::Id(id) = focus.field("id")? else {
662− return Err(Crash::new("focus has no id"));
663− };
664− let mut ids: Vec<String> = j::repo::all_commits(repo)?
665− .into_iter()
666− .filter_map(|commit| match commit.field("id").ok()? {
667− Value::Id(id) => Some(id.to_string()),
668− _ => None,
669− })
670− .collect();
671− ids.sort();
672− let prefix = j::eval::unique_prefix_in(&ids, &id);
673− let message = focus.field("message")?.as_text()?.to_string();
674− let title: String = message.lines().next().unwrap_or("").chars()
675− .map(|ch| if ch.is_control() { ' ' } else { ch }).take(72).collect();
676− let title = if title.trim().is_empty() { "(no description)" } else { title.trim() };
677− Ok(format!("focus @{} {}", prefix, title))
678− };
679− summary().unwrap_or_else(|_| "repository saved".to_string())
680−}
681−
682−fn is_repo_value(v: &Value) -> bool {
683 // §4.4: shape Repo and root's shape Commit
371 // §4.4: shape Repo and root's shape Commit
684 let fields = match v {
372 let fields = match v {
685 Value::Record(m) => m.keys().cloned().collect::<BTreeSet<String>>(),
373 Value::Record(m) => m.keys().cloned().collect::<BTreeSet<String>>(),
@@ -704,18 +392,6 @@ fn is_repo_value(v: &Value) -> bool {
704 .iter()
392 .iter()
705 .map(|s| s.to_string())
393 .map(|s| s.to_string())
706 .collect();
394 .collect();
395+ let _ = interp;
707 root_fields == want_root
396 root_fields == want_root
708}
397}
709−
710−#[cfg(test)]
711−mod tests {
712− #[test]
713− fn default_config_does_not_replace_a_file_created_since_the_read() {
714− let dir = tempfile::tempdir().unwrap();
715− let path = dir.path().join("config.j");
716− std::fs::write(&path, "user-owned configuration").unwrap();
717− let error = super::write_default_config(path.to_str().unwrap()).unwrap_err();
718− assert_eq!(error.kind(), std::io::ErrorKind::AlreadyExists);
719− assert_eq!(std::fs::read_to_string(path).unwrap(), "user-owned configuration");
720− }
721−}
src/parse.rs+162−346
@@ -1,9 +1,9 @@
1//! Parser for the j language (§3.2–3.4), including the two layout rules.
1//! Parser for the j language (§3.2–3.4), including the two layout rules.
2
2
3−use crate::ast::{Expr, Item, Pattern, Source, TypeExpr};
3+use crate::ast::{Expr, Item, Pattern, TypeExpr};
4use crate::lex::{lex, LexError, SpTok, Tok};
4use crate::lex::{lex, LexError, SpTok, Tok};
5use num_bigint::BigInt;
5use num_bigint::BigInt;
6−use std::collections::{BTreeSet, HashMap, HashSet};
6+use std::collections::BTreeSet;
7use std::rc::Rc;
7use std::rc::Rc;
8
8
9#[derive(Debug)]
9#[derive(Debug)]
@@ -31,45 +31,15 @@ impl From<LexError> for ParseError {
31}
31}
32
32
33pub struct Parser {
33pub struct Parser {
34− /// the text being parsed, of which each lambda keeps its span (§5.2)
35− src: Rc<str>,
36 toks: Vec<SpTok>,
34 toks: Vec<SpTok>,
37 pos: usize,
35 pos: usize,
38− /// Names in scope (top-level + builtins) for the no-shadowing rule;
36+ /// Names in scope (top-level + builtins) for the no-shadowing rule.
39− /// in config.j the builtins alone (§4.2).
40 outer_names: Rc<BTreeSet<String>>,
37 outer_names: Rc<BTreeSet<String>>,
41− /// Names bound by the lambdas and `let` blocks around the current
42− /// position, for the same rule (§4.2). Since shadowing is refused, no
43− /// name is in here twice, and a construct removes the names it added
44− /// when its scope ends.
45− locals: HashSet<String>,
46− /// How many local binders have been parsed, and for each name the
47− /// ordinal and line of the latest one: the `let` check that `locals`
48− /// cannot make (see `parse_let`).
49− binders: usize,
50− last_binder: HashMap<String, (usize, usize)>,
51 /// set when an operator expression ends with a trailing operator before
38 /// set when an operator expression ends with a trailing operator before
52 /// `)`, i.e. a left section
39 /// `)`, i.e. a left section
53 section_op: Option<&'static str>,
40 section_op: Option<&'static str>,
54− /// levels of recursive descent currently open, bounded by `MAX_DEPTH`
55− depth: usize,
56− /// how many levels high (see `MAX_DEPTH`) the expression the last parse
57− /// function returned is, which each leaves here for its caller
58− height: usize,
59}
41}
60
42
61−/// How many levels deep an expression may be (§3.4): a leaf is one level, and
62−/// any other node one more than its highest part, or two more for a
63−/// parenthesis (or section) and for the braces of a record or update. Deeper
64−/// input is a parse error (§1.4) instead of an overflow of the worker
65−/// thread's stack (main.rs). The recursive descent opens at most one level of
66−/// its own (`nested`) per level of the expression around the point it has
67−/// reached, each taking up to about 6 KB of stack in a debug build (a
68−/// parenthesis takes two) and 1.3 KB in a release build, so the deepest parse
69−/// takes under half of the worker's 512 MB. The passes over the tree after the
70−/// parse, and dropping it, recurse on its height at about 1.2 KB a level.
71−const MAX_DEPTH: usize = 40_000;
72−
73/// fixity table (§3.2): (precedence, right-assoc)
43/// fixity table (§3.2): (precedence, right-assoc)
74fn fixity(op: &str) -> Option<(u8, bool)> {
44fn fixity(op: &str) -> Option<(u8, bool)> {
75 Some(match op {
45 Some(match op {
@@ -92,51 +62,13 @@ fn is_nonassoc(op: &str) -> bool {
92impl Parser {
62impl Parser {
93 pub fn new(src: &str, outer_names: Rc<BTreeSet<String>>) -> Result<Self, ParseError> {
63 pub fn new(src: &str, outer_names: Rc<BTreeSet<String>>) -> Result<Self, ParseError> {
94 Ok(Parser {
64 Ok(Parser {
95− src: Rc::from(src),
96 toks: lex(src)?,
65 toks: lex(src)?,
97 pos: 0,
66 pos: 0,
98 outer_names,
67 outer_names,
99− locals: HashSet::new(),
100− binders: 0,
101− last_binder: HashMap::new(),
102 section_op: None,
68 section_op: None,
103− depth: 0,
104− height: 0,
105 })
69 })
106 }
70 }
107
71
108− /// Run `f` one level deeper in the recursive descent. Every cycle of
109− /// mutually recursive parse functions passes through a call made here,
110− /// which is what bounds the parser's stack use. Each call is matched by a
111− /// level of the expression around what `f` parses, so on input that
112− /// parses, this bound is reached no sooner than `within`'s: it is what
113− /// stops input that never closes, like `((((…`.
114− fn nested<T>(
115− &mut self,
116− f: impl FnOnce(&mut Self) -> Result<T, ParseError>,
117− ) -> Result<T, ParseError> {
118− if self.depth >= MAX_DEPTH {
119− return Err(ParseError::new("nested too deeply", self.line()));
120− }
121− self.depth += 1;
122− let r = f(self);
123− self.depth -= 1;
124− r
125− }
126−
127− /// `h`, the height of an expression just built, while it is within
128− /// `MAX_DEPTH`. Every node is checked here as it is built, since a loop
129− /// builds a left-associative chain (`a + b + …`, `f x y …`, `r.a.b …`,
130− /// `r { … } { … }`) as tall as it is long without recursing, and the
131− /// passes over the tree after the parse, and dropping it, recurse on its
132− /// height.
133− fn within(&self, h: usize) -> Result<usize, ParseError> {
134− if h > MAX_DEPTH {
135− return Err(ParseError::new("nested too deeply", self.line()));
136− }
137− Ok(h)
138− }
139−
140 fn peek(&self) -> &Tok {
72 fn peek(&self) -> &Tok {
141 &self.toks[self.pos].tok
73 &self.toks[self.pos].tok
142 }
74 }
@@ -191,15 +123,12 @@ impl Parser {
191 Ok(items)
123 Ok(items)
192 }
124 }
193
125
194− /// One item. Under rule 1 (§3.3) a line starting past column 1 continues
195− /// it, so a line break before its `=` or `:` is skipped when the next
196− /// line starts past column 1, like one inside its type or expression.
197 fn parse_item(&mut self) -> Result<Item, ParseError> {
126 fn parse_item(&mut self) -> Result<Item, ParseError> {
198 match self.peek().clone() {
127 match self.peek().clone() {
199 Tok::TypeName(name) => {
128 Tok::TypeName(name) => {
200 // typedecl: TYPENAME '=' type
129 // typedecl: TYPENAME '=' type
201 self.bump();
130 self.bump();
202− self.skip_layout_newlines(1);
131+ self.skip_newlines();
203 self.expect(&Tok::Equals)?;
132 self.expect(&Tok::Equals)?;
204 let ty = self.parse_type()?;
133 let ty = self.parse_type()?;
205 Ok(Item::TypeDecl(name, ty))
134 Ok(Item::TypeDecl(name, ty))
@@ -207,7 +136,6 @@ impl Parser {
207 Tok::Ident(name) => {
136 Tok::Ident(name) => {
208 let line = self.line();
137 let line = self.line();
209 self.bump();
138 self.bump();
210− self.skip_layout_newlines(1);
211 match self.peek().clone() {
139 match self.peek().clone() {
212 Tok::Colon => {
140 Tok::Colon => {
213 self.bump();
141 self.bump();
@@ -216,8 +144,7 @@ impl Parser {
216 }
144 }
217 Tok::Equals => {
145 Tok::Equals => {
218 self.bump();
146 self.bump();
219− // inside nothing, so it opens no level (see `nested`)
220− let e = self.parse_expr_inner(1)?;
147+ let e = self.parse_expr(1)?;
221 Ok(Item::Definition(name, Rc::new(e), line))
148 Ok(Item::Definition(name, Rc::new(e), line))
222 }
149 }
223 other => Err(ParseError::new(
150 other => Err(ParseError::new(
@@ -240,7 +167,6 @@ impl Parser {
240 }
167 }
241 };
168 };
242 self.expect(&Tok::RParen)?;
169 self.expect(&Tok::RParen)?;
243− self.skip_layout_newlines(1);
244 self.expect(&Tok::Colon)?;
170 self.expect(&Tok::Colon)?;
245 let ty = self.parse_type()?;
171 let ty = self.parse_type()?;
246 Ok(Item::Signature(op.to_string(), ty, line))
172 Ok(Item::Signature(op.to_string(), ty, line))
@@ -256,16 +182,9 @@ impl Parser {
256 // types
182 // types
257 // ------------------------------------------------------------------
183 // ------------------------------------------------------------------
258 fn parse_type(&mut self) -> Result<TypeExpr, ParseError> {
184 fn parse_type(&mut self) -> Result<TypeExpr, ParseError> {
259− self.nested(Self::parse_type_inner)
185+ self.skip_newlines_in_type();
260− }
261−
262− /// Types occur only in top-level items, so their layout limit is always
263− /// rule 1's column 1 (§3.3): a line break may fall anywhere in a type,
264− /// before or after `->` alike, when the next line starts past column 1.
265− fn parse_type_inner(&mut self) -> Result<TypeExpr, ParseError> {
266− self.skip_layout_newlines(1);
267 let lhs = self.parse_atype()?;
186 let lhs = self.parse_atype()?;
268− self.skip_layout_newlines(1);
187+ self.skip_newlines_in_type();
269 if matches!(self.peek(), Tok::Arrow) {
188 if matches!(self.peek(), Tok::Arrow) {
270 self.bump();
189 self.bump();
271 let rhs = self.parse_type()?;
190 let rhs = self.parse_type()?;
@@ -275,6 +194,38 @@ impl Parser {
275 }
194 }
276 }
195 }
277
196
197+ /// In types (signatures) we allow newlines before `->` continuation when
198+ /// indented; keep it simple: skip newlines if the next token is `->`.
199+ fn skip_newlines_in_type(&mut self) {
200+ let save = self.pos;
201+ loop {
202+ if matches!(self.peek(), Tok::Newline) {
203+ self.pos += 1;
204+ // only continue skipping if Arrow follows after the newlines
205+ let _ = &save;
206+ let mut p = self.pos;
207+ while matches!(self.toks[p].tok, Tok::Newline) {
208+ p += 1;
209+ }
210+ if matches!(self.toks[p].tok, Tok::Arrow) {
211+ self.pos = p;
212+ return;
213+ } else {
214+ self.pos = save;
215+ // restore to before newlines? we must not consume them if
216+ // not followed by Arrow, since layout needs them.
217+ // Walk back: find first newline we consumed.
218+ while self.pos > 0 && matches!(self.toks[self.pos - 1].tok, Tok::Newline) {
219+ self.pos -= 1;
220+ }
221+ return;
222+ }
223+ } else {
224+ return;
225+ }
226+ }
227+ }
228+
278 fn parse_atype(&mut self) -> Result<TypeExpr, ParseError> {
229 fn parse_atype(&mut self) -> Result<TypeExpr, ParseError> {
279 match self.peek().clone() {
230 match self.peek().clone() {
280 Tok::TypeName(n) => {
231 Tok::TypeName(n) => {
@@ -295,7 +246,7 @@ impl Parser {
295 self.bump();
246 self.bump();
296 let mut fields = Vec::new();
247 let mut fields = Vec::new();
297 loop {
248 loop {
298− self.skip_layout_newlines(1);
249+ self.skip_newlines();
299 if matches!(self.peek(), Tok::RBrace) {
250 if matches!(self.peek(), Tok::RBrace) {
300 self.bump();
251 self.bump();
301 break;
252 break;
@@ -309,11 +260,10 @@ impl Parser {
309 ))
260 ))
310 }
261 }
311 };
262 };
312− self.skip_layout_newlines(1);
313 self.expect(&Tok::Colon)?;
263 self.expect(&Tok::Colon)?;
314 let t = self.parse_type()?;
264 let t = self.parse_type()?;
315 fields.push((name, Rc::new(t)));
265 fields.push((name, Rc::new(t)));
316− self.skip_layout_newlines(1);
266+ self.skip_newlines();
317 match self.peek() {
267 match self.peek() {
318 Tok::Comma => {
268 Tok::Comma => {
319 self.bump();
269 self.bump();
@@ -353,84 +303,59 @@ impl Parser {
353 /// expression stops at a Newline whose following token is in a column
303 /// expression stops at a Newline whose following token is in a column
354 /// <= min_col (it belongs to an outer construct).
304 /// <= min_col (it belongs to an outer construct).
355 pub fn parse_expr(&mut self, min_col: usize) -> Result<Expr, ParseError> {
305 pub fn parse_expr(&mut self, min_col: usize) -> Result<Expr, ParseError> {
356− self.nested(|p| p.parse_expr_inner(min_col))
357− }
358−
359− /// Each construct here and in `parse_atom_inner` is parsed by a function
360− /// of its own: nested input stacks a frame of every function on its path
361− /// for each level (see `MAX_DEPTH`), and in a debug build a function's
362− /// frame holds the temporaries of all its arms.
363− fn parse_expr_inner(&mut self, min_col: usize) -> Result<Expr, ParseError> {
364 self.skip_layout_newlines(min_col);
306 self.skip_layout_newlines(min_col);
365 match self.peek().clone() {
307 match self.peek().clone() {
366− Tok::Backslash => self.parse_lambda(min_col),
308+ Tok::Backslash => {
367− Tok::If => self.parse_if(min_col),
368− Tok::Let => self.parse_let(min_col),
369− _ => self.parse_or(min_col),
370− }
371− }
372−
373− fn parse_lambda(&mut self, min_col: usize) -> Result<Expr, ParseError> {
374− let src_start = self.toks[self.pos].clone();
309+ let src_start = self.toks[self.pos].clone();
375− self.bump();
310+ self.bump();
376− let mut params = Vec::new();
311+ let mut params = Vec::new();
377− loop {
312+ loop {
378− match self.peek().clone() {
313+ match self.peek().clone() {
379− Tok::Ident(n) => {
314+ Tok::Ident(n) => {
380− // in scope from here on, so a later parameter
381− // may not reuse it either
382− self.check_shadow(&n)?;
315+ self.check_shadow(&n)?;
383− self.locals.insert(n.clone());
384− self.note_binder(&n, self.line());
385− params.push(Pattern::Var(n));
316+ params.push(Pattern::Var(n));
386− self.bump();
317+ self.bump();
387− }
318+ }
388− Tok::Underscore => {
319+ Tok::Underscore => {
389− params.push(Pattern::Wildcard);
320+ params.push(Pattern::Wildcard);
390− self.bump();
321+ self.bump();
391− }
322+ }
392− Tok::Arrow => {
323+ Tok::Arrow => {
393− self.bump();
324+ self.bump();
394− break;
325+ break;
326+ }
327+ other => {
328+ return Err(ParseError::new(
329+ format!(
330+ "expected a parameter or `->`, found {}",
331+ other.describe()
332+ ),
333+ self.line(),
334+ ))
335+ }
336+ }
395 }
337 }
396− other => {
338+ if params.is_empty() {
397− return Err(ParseError::new(
339+ return Err(ParseError::new("lambda needs at least one parameter", self.line()));
398− format!(
399− "expected a parameter or `->`, found {}",
400− other.describe()
401− ),
402− self.line(),
403− ))
404 }
340 }
341+ let body = self.parse_expr(min_col)?;
342+ let src = self.source_slice(&src_start);
343+ Ok(Expr::Lambda(params, Rc::new(body), src))
405 }
344 }
406− }
345+ Tok::If => {
407− if params.is_empty() {
346+ self.bump();
408− return Err(ParseError::new("lambda needs at least one parameter", self.line()));
347+ let c = self.parse_expr(min_col)?;
409− }
348+ self.skip_layout_newlines(min_col);
349+ self.expect(&Tok::Then)?;
410− let body = self.parse_expr(min_col)?;
350+ let t = self.parse_expr(min_col)?;
411− self.height = self.within(self.height + 1)?;
351+ self.skip_layout_newlines(min_col);
412− for p in &params {
352+ self.expect(&Tok::Else)?;
413− if let Pattern::Var(n) = p {
353+ let e = self.parse_expr(min_col)?;
414− self.locals.remove(n);
354+ Ok(Expr::If(Rc::new(c), Rc::new(t), Rc::new(e)))
415 }
355 }
356+ Tok::Let => self.parse_let(min_col),
357+ _ => self.parse_or(min_col),
416 }
358 }
417− let src = self.source_slice(&src_start);
418− Ok(Expr::Lambda(params, Rc::new(body), src))
419− }
420−
421− fn parse_if(&mut self, min_col: usize) -> Result<Expr, ParseError> {
422− self.bump();
423− let c = self.parse_expr(min_col)?;
424− let mut h = self.height;
425− self.skip_layout_newlines(min_col);
426− self.expect(&Tok::Then)?;
427− let t = self.parse_expr(min_col)?;
428− h = h.max(self.height);
429− self.skip_layout_newlines(min_col);
430− self.expect(&Tok::Else)?;
431− let e = self.parse_expr(min_col)?;
432− self.height = self.within(h.max(self.height) + 1)?;
433− Ok(Expr::If(Rc::new(c), Rc::new(t), Rc::new(e)))
434 }
359 }
435
360
436 /// skip Newlines whose next real token is in a column > min_col
361 /// skip Newlines whose next real token is in a column > min_col
@@ -471,9 +396,7 @@ impl Parser {
471 ));
396 ));
472 }
397 }
473 let mut bindings: Vec<(String, Rc<Expr>)> = Vec::new();
398 let mut bindings: Vec<(String, Rc<Expr>)> = Vec::new();
474− let mut names: Vec<(String, usize)> = Vec::new();
399+ let mut names: Vec<String> = Vec::new();
475− let first_binder = self.binders;
476− let mut h = 0;
477 loop {
400 loop {
478 // binding: ident '=' expr, expr limited to block_col
401 // binding: ident '=' expr, expr limited to block_col
479 let name = match self.peek().clone() {
402 let name = match self.peek().clone() {
@@ -485,21 +408,17 @@ impl Parser {
485 ))
408 ))
486 }
409 }
487 };
410 };
411+ self.check_shadow(&name)?;
488− if names.iter().any(|(n, _)| *n == name) {
412+ if names.iter().any(|n| *n == name) {
489 return Err(ParseError::new(
413 return Err(ParseError::new(
490 format!("`{}` is bound twice in this let", name),
414 format!("`{}` is bound twice in this let", name),
491 self.line(),
415 self.line(),
492 ));
416 ));
493 }
417 }
494− self.check_shadow(&name)?;
495− // in scope from here on: in its own expression, the later ones
496− // and the body (§4.1)
497− self.locals.insert(name.clone());
498− names.push((name.clone(), self.line()));
499 self.bump();
418 self.bump();
500 self.expect(&Tok::Equals)?;
419 self.expect(&Tok::Equals)?;
501 let e = self.parse_expr(block_col)?;
420 let e = self.parse_expr(block_col)?;
502− h = h.max(self.height);
421+ names.push(name.clone());
503 bindings.push((name, Rc::new(e)));
422 bindings.push((name, Rc::new(e)));
504 // separator: `;` or newline at exactly block_col, then `in` ends
423 // separator: `;` or newline at exactly block_col, then `in` ends
505 match self.peek().clone() {
424 match self.peek().clone() {
@@ -524,16 +443,6 @@ impl Parser {
524 }
443 }
525 let next_col = self.toks[q].col;
444 let next_col = self.toks[q].col;
526 if matches!(self.toks[q].tok, Tok::In) {
445 if matches!(self.toks[q].tok, Tok::In) {
527− // like any token that begins a line, an `in` at or
528− // left of the limit belongs to an outer construct
529− // (§3.3): a new top-level item or an outer let's
530− // binding line, never this let
531− if next_col <= min_col {
532− return Err(ParseError::new(
533− format!("`in` must be indented past column {}", min_col),
534− self.toks[q].line,
535− ));
536− }
537 self.pos = q;
446 self.pos = q;
538 self.bump();
447 self.bump();
539 break;
448 break;
@@ -571,66 +480,39 @@ impl Parser {
571 }
480 }
572 }
481 }
573 }
482 }
574− // A binding is in scope in the expressions before it too (§4.1), but
575− // it was not yet in `locals` while they were parsed: a binder in one
576− // of them that reuses its name is caught here instead.
577− for (n, _) in &names {
578− if let Some(&(k, line)) = self.last_binder.get(n) {
579− if k >= first_binder {
580− return Err(shadowed(n, line));
581− }
582− }
583− }
584− for (n, line) in &names {
585− self.note_binder(n, *line);
586− }
587 let body = self.parse_expr(min_col)?;
483 let body = self.parse_expr(min_col)?;
588− self.height = self.within(h.max(self.height) + 1)?;
589− for (n, _) in &names {
590− self.locals.remove(n);
591− }
592 Ok(Expr::Let(bindings, Rc::new(body)))
484 Ok(Expr::Let(bindings, Rc::new(body)))
593 }
485 }
594
486
595− /// §4.2: a binder may not reuse a top-level or builtin name, nor one
596− /// bound by an enclosing lambda or `let`.
597 fn check_shadow(&self, name: &str) -> Result<(), ParseError> {
487 fn check_shadow(&self, name: &str) -> Result<(), ParseError> {
598− if self.outer_names.contains(name) || self.locals.contains(name) {
488+ if self.outer_names.contains(name) {
489+ return Err(ParseError::new(
490+ format!("`{}` is already bound and may not be shadowed", name),
599− return Err(shadowed(name, self.line()));
491+ self.line(),
492+ ));
600 }
493 }
601 Ok(())
494 Ok(())
602 }
495 }
603
496
604− /// Record a local binder for the check at the end of each enclosing
605− /// `let` block's bindings.
606− fn note_binder(&mut self, name: &str, line: usize) {
607− self.last_binder.insert(name.to_string(), (self.binders, line));
608− self.binders += 1;
609− }
610−
611 fn parse_or(&mut self, min_col: usize) -> Result<Expr, ParseError> {
497 fn parse_or(&mut self, min_col: usize) -> Result<Expr, ParseError> {
612 // `or` is infixl 0 and extends as far right as possible
498 // `or` is infixl 0 and extends as far right as possible
613 let mut lhs = self.parse_opexpr(1, min_col)?;
499 let mut lhs = self.parse_opexpr(1, min_col)?;
614− let mut h = self.height;
615 loop {
500 loop {
616 self.skip_layout_newlines(min_col);
501 self.skip_layout_newlines(min_col);
617 if matches!(self.peek(), Tok::Or) {
502 if matches!(self.peek(), Tok::Or) {
618 self.bump();
503 self.bump();
619− let rhs = self.nested(|p| p.parse_or(min_col))?;
504+ let rhs = self.parse_or(min_col)?;
620− h = self.within(h.max(self.height) + 1)?;
621 lhs = Expr::Or(Rc::new(lhs), Rc::new(rhs));
505 lhs = Expr::Or(Rc::new(lhs), Rc::new(rhs));
622 } else {
506 } else {
623 break;
507 break;
624 }
508 }
625 }
509 }
626− self.height = h;
627 Ok(lhs)
510 Ok(lhs)
628 }
511 }
629
512
630 /// precedence-climbing over the fixity table
513 /// precedence-climbing over the fixity table
631 fn parse_opexpr(&mut self, min_prec: u8, min_col: usize) -> Result<Expr, ParseError> {
514 fn parse_opexpr(&mut self, min_prec: u8, min_col: usize) -> Result<Expr, ParseError> {
632 let mut lhs = self.parse_app(min_col)?;
515 let mut lhs = self.parse_app(min_col)?;
633− let mut h = self.height;
634 loop {
516 loop {
635 self.skip_op_newline(min_col);
517 self.skip_op_newline(min_col);
636 let op: &'static str = match self.peek() {
518 let op: &'static str = match self.peek() {
@@ -648,11 +530,10 @@ impl Parser {
648 // left section: `(e op)` — a trailing operator before `)`
530 // left section: `(e op)` — a trailing operator before `)`
649 if matches!(self.peek(), Tok::RParen) {
531 if matches!(self.peek(), Tok::RParen) {
650 self.section_op = Some(op);
532 self.section_op = Some(op);
651− self.height = h;
652 return Ok(lhs);
533 return Ok(lhs);
653 }
534 }
654 let next_min = if right { prec } else { prec + 1 };
535 let next_min = if right { prec } else { prec + 1 };
655− let rhs = self.nested(|p| p.parse_opexpr(next_min, min_col))?;
536+ let rhs = self.parse_opexpr(next_min, min_col)?;
656 // non-assoc check: if lhs is the same operator, reject chaining
537 // non-assoc check: if lhs is the same operator, reject chaining
657 if is_nonassoc(op) {
538 if is_nonassoc(op) {
658 // a non-associative operator cannot be chained; explicit
539 // a non-associative operator cannot be chained; explicit
@@ -674,10 +555,8 @@ impl Parser {
674 }
555 }
675 }
556 }
676 }
557 }
677− h = self.within(h.max(self.height) + 1)?;
678 lhs = Expr::BinOp(op, Rc::new(lhs), Rc::new(rhs));
558 lhs = Expr::BinOp(op, Rc::new(lhs), Rc::new(rhs));
679 }
559 }
680− self.height = h;
681 Ok(lhs)
560 Ok(lhs)
682 }
561 }
683
562
@@ -717,7 +596,6 @@ impl Parser {
717
596
718 fn parse_app(&mut self, min_col: usize) -> Result<Expr, ParseError> {
597 fn parse_app(&mut self, min_col: usize) -> Result<Expr, ParseError> {
719 let mut e = self.parse_postfix(min_col)?;
598 let mut e = self.parse_postfix(min_col)?;
720− let mut h = self.height;
721 loop {
599 loop {
722 // application across a newline requires deeper indentation
600 // application across a newline requires deeper indentation
723 if matches!(self.peek(), Tok::Newline) {
601 if matches!(self.peek(), Tok::Newline) {
@@ -733,24 +611,20 @@ impl Parser {
733 }
611 }
734 if Self::starts_atom(self.peek()) {
612 if Self::starts_atom(self.peek()) {
735 let arg = self.parse_postfix(min_col)?;
613 let arg = self.parse_postfix(min_col)?;
736− h = self.within(h.max(self.height) + 1)?;
737 e = Expr::App(Rc::new(e), Rc::new(arg));
614 e = Expr::App(Rc::new(e), Rc::new(arg));
738 } else {
615 } else {
739 break;
616 break;
740 }
617 }
741 }
618 }
742− self.height = h;
743 Ok(e)
619 Ok(e)
744 }
620 }
745
621
746 fn parse_postfix(&mut self, min_col: usize) -> Result<Expr, ParseError> {
622 fn parse_postfix(&mut self, min_col: usize) -> Result<Expr, ParseError> {
747 let mut e = self.parse_atom(min_col)?;
623 let mut e = self.parse_atom(min_col)?;
748− let mut h = self.height;
749 loop {
624 loop {
750 match self.peek().clone() {
625 match self.peek().clone() {
751 Tok::Selector(f) => {
626 Tok::Selector(f) => {
752 self.bump();
627 self.bump();
753− h = self.within(h + 1)?;
754 e = Expr::Select(Rc::new(e), f);
628 e = Expr::Select(Rc::new(e), f);
755 }
629 }
756 Tok::LBrace => {
630 Tok::LBrace => {
@@ -758,18 +632,12 @@ impl Parser {
758 // postfix binds tighter than application, so `{` after an
632 // postfix binds tighter than application, so `{` after an
759 // atom is an update)
633 // atom is an update)
760 self.bump();
634 self.bump();
761− // two levels, like a record literal's braces, so a second
762− // level of the descent to match (see `MAX_DEPTH`)
763− let fields = self.nested(|p| p.parse_fields(min_col, true))?;
635+ let fields = self.parse_fields(min_col, true)?;
764− h = self.within(h.max(self.height) + 2)?;
765 e = Expr::Update(Rc::new(e), fields);
636 e = Expr::Update(Rc::new(e), fields);
766 }
637 }
767− // a Newline ends the chain too: a selector or `{` that
768− // begins a line is the next postfix, not this one's (§3.3)
769 _ => break,
638 _ => break,
770 }
639 }
771 }
640 }
772− self.height = h;
773 Ok(e)
641 Ok(e)
774 }
642 }
775
643
@@ -783,12 +651,8 @@ impl Parser {
783 self.skip_layout_newlines(min_col);
651 self.skip_layout_newlines(min_col);
784 if matches!(self.peek(), Tok::RBrace) {
652 if matches!(self.peek(), Tok::RBrace) {
785 self.bump();
653 self.bump();
786− // nothing inside the braces
787− self.height = 0;
788 return Ok(fields);
654 return Ok(fields);
789 }
655 }
790− // the height of the highest field
791− let mut h = 0;
792 loop {
656 loop {
793 let name = match self.bump() {
657 let name = match self.bump() {
794 Tok::Ident(n) => n,
658 Tok::Ident(n) => n,
@@ -807,7 +671,6 @@ impl Parser {
807 }
671 }
808 self.expect(&Tok::Equals)?;
672 self.expect(&Tok::Equals)?;
809 let v = self.parse_expr(min_col)?;
673 let v = self.parse_expr(min_col)?;
810− h = h.max(self.height);
811 fields.push((name, Rc::new(v)));
674 fields.push((name, Rc::new(v)));
812 self.skip_layout_newlines(min_col);
675 self.skip_layout_newlines(min_col);
813 match self.peek() {
676 match self.peek() {
@@ -827,17 +690,10 @@ impl Parser {
827 }
690 }
828 }
691 }
829 }
692 }
830− self.height = h;
831 Ok(fields)
693 Ok(fields)
832 }
694 }
833
695
834 fn parse_atom(&mut self, min_col: usize) -> Result<Expr, ParseError> {
696 fn parse_atom(&mut self, min_col: usize) -> Result<Expr, ParseError> {
835− self.nested(|p| p.parse_atom_inner(min_col))
836− }
837−
838− fn parse_atom_inner(&mut self, min_col: usize) -> Result<Expr, ParseError> {
839− // one level, unless an arm below builds more around what it parses
840− self.height = 1;
841 match self.peek().clone() {
697 match self.peek().clone() {
842 Tok::Ident(n) => {
698 Tok::Ident(n) => {
843 self.bump();
699 self.bump();
@@ -883,16 +739,70 @@ impl Parser {
883 self.bump();
739 self.bump();
884 Ok(Expr::SelectorFun(f))
740 Ok(Expr::SelectorFun(f))
885 }
741 }
886− Tok::LBracket => self.parse_list(min_col),
742+ Tok::LBracket => {
743+ self.bump();
744+ let mut elems = Vec::new();
745+ loop {
746+ self.skip_layout_newlines(min_col);
747+ if matches!(self.peek(), Tok::RBracket) {
748+ self.bump();
749+ break;
750+ }
751+ let e = self.parse_postfix(min_col)?;
752+ elems.push(Rc::new(e));
753+ }
754+ Ok(Expr::List(elems))
755+ }
887 Tok::LBrace => {
756 Tok::LBrace => {
888 self.bump();
757 self.bump();
889 let fields = self.parse_fields(min_col, false)?;
758 let fields = self.parse_fields(min_col, false)?;
890− // the braces are two levels, as they open two of the
891− // descent: this atom and the field (see `MAX_DEPTH`)
892− self.height = self.within(self.height + 2)?;
893 Ok(Expr::Record(fields))
759 Ok(Expr::Record(fields))
894 }
760 }
895− Tok::LParen => self.parse_paren(min_col),
761+ Tok::LParen => {
762+ self.bump();
763+ // (op) | (op expr) | (expr op) | (expr)
764+ if let Tok::Op(o) = self.peek().clone() {
765+ self.bump();
766+ self.skip_layout_newlines(min_col);
767+ if matches!(self.peek(), Tok::RParen) {
768+ self.bump();
769+ return Ok(Expr::Var(o.to_string()));
770+ }
771+ // right section: (op e) = \x -> x op e
772+ let e = self.parse_expr(min_col)?;
773+ self.expect(&Tok::RParen)?;
774+ let var = fresh_var();
775+ let body = Expr::BinOp(
776+ o,
777+ Rc::new(Expr::Var(var.clone())),
778+ Rc::new(e),
779+ );
780+ return Ok(Expr::Lambda(
781+ vec![Pattern::Var(var)],
782+ Rc::new(body),
783+ "section".into(),
784+ ));
785+ }
786+ let first = self.parse_expr(min_col)?;
787+ self.skip_layout_newlines(min_col);
788+ if let Some(o) = self.section_op.take() {
789+ // left section: (e op) = \x -> e op x
790+ self.expect(&Tok::RParen)?;
791+ let var = fresh_var();
792+ let body = Expr::BinOp(
793+ o,
794+ Rc::new(first),
795+ Rc::new(Expr::Var(var.clone())),
796+ );
797+ return Ok(Expr::Lambda(
798+ vec![Pattern::Var(var)],
799+ Rc::new(body),
800+ "section".into(),
801+ ));
802+ }
803+ self.expect(&Tok::RParen)?;
804+ Ok(Expr::Paren(Rc::new(first)))
805+ }
896 other => Err(ParseError::new(
806 other => Err(ParseError::new(
897 format!("unexpected {}", other.describe()),
807 format!("unexpected {}", other.describe()),
898 self.line(),
808 self.line(),
@@ -900,105 +810,15 @@ impl Parser {
900 }
810 }
901 }
811 }
902
812
903− fn parse_list(&mut self, min_col: usize) -> Result<Expr, ParseError> {
904− self.bump();
905− let mut elems = Vec::new();
906− let mut h = 0;
907− loop {
908− self.skip_layout_newlines(min_col);
909− if matches!(self.peek(), Tok::RBracket) {
910− self.bump();
911− break;
912− }
913− let e = self.parse_postfix(min_col)?;
914− h = h.max(self.height);
915− elems.push(Rc::new(e));
916− }
917− self.height = self.within(h + 1)?;
918− Ok(Expr::List(elems))
919− }
920−
921− fn parse_paren(&mut self, min_col: usize) -> Result<Expr, ParseError> {
922− let open = self.toks[self.pos].clone();
923− self.bump();
924− // (op) | (op expr) | (expr op) | (expr)
925− if let Tok::Op(o) = self.peek().clone() {
926− self.bump();
927− self.skip_layout_newlines(min_col);
928− if matches!(self.peek(), Tok::RParen) {
929− self.bump();
930− return Ok(Expr::Var(o.to_string()));
931− }
932− // right section: (op e) = \x -> x op e
933− let e = self.parse_expr(min_col)?;
934− // `(op e op)` is no production (§3.4); unchecked, the
935− // trailing operator would be dropped here and left set
936− // for the next `(e)` to take as a left section
937− if let Some(o2) = self.section_op.take() {
938− return Err(ParseError::new(
939− format!(
940− "a section cannot have operators on both sides (`{}` and `{}`)",
941− o, o2
942− ),
943− self.line(),
944− ));
945− }
946− self.expect(&Tok::RParen)?;
947− // a lambda around an operator: two levels
948− self.height = self.within(self.height + 2)?;
949− let var = fresh_var();
950− let body = Expr::BinOp(
951− o,
952− Rc::new(Expr::Var(var.clone())),
953− Rc::new(e),
954− );
955− return Ok(Expr::Lambda(
956− vec![Pattern::Var(var)],
957− Rc::new(body),
958− self.source_slice(&open),
813+ fn source_slice(&self, _start: &SpTok) -> String {
959− ));
814+ // source text of a lambda for display; we re-render from the AST.
960− }
815+ String::new()
961− let first = self.parse_expr(min_col)?;
962− // a parenthesis is two levels, as is a section's lambda
963− // around its operator
964− self.height = self.within(self.height + 2)?;
965− self.skip_layout_newlines(min_col);
966− if let Some(o) = self.section_op.take() {
967− // left section: (e op) = \x -> e op x
968− self.expect(&Tok::RParen)?;
969− let var = fresh_var();
970− let body = Expr::BinOp(
971− o,
972− Rc::new(first),
973− Rc::new(Expr::Var(var.clone())),
974− );
975− return Ok(Expr::Lambda(
976− vec![Pattern::Var(var)],
977− Rc::new(body),
978− self.source_slice(&open),
979− ));
980− }
981− self.expect(&Tok::RParen)?;
982− Ok(Expr::Paren(Rc::new(first)))
983− }
984−
985− /// The source of a lambda or section, from its first token `start` to
986− /// the last token consumed, for its rendering (§5.2: exactly as
987− /// written). Line breaks the layout rules skipped looking for more of it
988− /// are not part of it, nor is a comment after its last token.
989− fn source_slice(&self, start: &SpTok) -> Source {
990− let mut last = self.pos - 1;
991− while matches!(self.toks[last].tok, Tok::Newline) {
992− last -= 1;
993− }
994− Source::new(self.src.clone(), start.start, self.toks[last].end)
995 }
816 }
996
817
997 /// Parse a single command-line expression. Layout rule 1 does not apply.
818 /// Parse a single command-line expression. Layout rule 1 does not apply.
998 pub fn parse_cli_expr(&mut self) -> Result<Expr, ParseError> {
819 pub fn parse_cli_expr(&mut self) -> Result<Expr, ParseError> {
999 self.skip_newlines();
820 self.skip_newlines();
1000− // inside nothing, so it opens no level (see `nested`)
1001− let e = self.parse_expr_inner(0)?;
821+ let e = self.parse_expr(0)?;
1002 self.skip_newlines();
822 self.skip_newlines();
1003 if !matches!(self.peek(), Tok::Eof) {
823 if !matches!(self.peek(), Tok::Eof) {
1004 return Err(ParseError::new(
824 return Err(ParseError::new(
@@ -1010,10 +830,6 @@ impl Parser {
1010 }
830 }
1011}
831}
1012
832
1013−fn shadowed(name: &str, line: usize) -> ParseError {
1014− ParseError::new(format!("`{}` is already bound and may not be shadowed", name), line)
1015−}
1016−
1017use std::sync::atomic::{AtomicUsize, Ordering};
833use std::sync::atomic::{AtomicUsize, Ordering};
1018static FRESH: AtomicUsize = AtomicUsize::new(0);
834static FRESH: AtomicUsize = AtomicUsize::new(0);
1019fn fresh_var() -> String {
835fn fresh_var() -> String {
src/render.rs+570−1668
@@ -2,11 +2,8 @@
2
2
3use crate::domain::ROOT_ID;
3use crate::domain::ROOT_ID;
4use crate::eval::Interp;
4use crate::eval::Interp;
5−use crate::value::{Crash, ListVal, RecordMap, ShapeKind, Value};
5+use crate::value::{Crash, ShapeKind, Value};
6−use std::cmp::Reverse;
7−use std::collections::{BTreeMap, BTreeSet, BinaryHeap};
6+use std::collections::{BTreeMap, BTreeSet};
8−use std::ops::Bound;
9−use std::rc::Rc;
10use unicode_width::UnicodeWidthChar;
7use unicode_width::UnicodeWidthChar;
11
8
12// ----------------------------------------------------------------------
9// ----------------------------------------------------------------------
@@ -33,46 +30,33 @@ impl Palette {
33 self.wrap("2", s)
30 self.wrap("2", s)
34 }
31 }
35 pub fn bold_bg(&self, s: &str) -> String {
32 pub fn bold_bg(&self, s: &str) -> String {
36− self.wrap("1;97;48;5;17", s)
33+ self.wrap("1;48;5;236", s)
37 }
34 }
38 pub fn dim_italic(&self, s: &str) -> String {
35 pub fn dim_italic(&self, s: &str) -> String {
39 self.wrap("2;3", s)
36 self.wrap("2;3", s)
40 }
37 }
41 pub fn cyan(&self, s: &str) -> String {
38 pub fn cyan(&self, s: &str) -> String {
42− self.wrap("96", s)
39+ self.wrap("36", s)
43 }
40 }
44 pub fn red(&self, s: &str) -> String {
41 pub fn red(&self, s: &str) -> String {
45− self.wrap("91", s)
42+ self.wrap("31", s)
46 }
43 }
47 pub fn blue(&self, s: &str) -> String {
44 pub fn blue(&self, s: &str) -> String {
48− self.wrap("94", s)
45+ self.wrap("34", s)
49 }
46 }
50 pub fn green(&self, s: &str) -> String {
47 pub fn green(&self, s: &str) -> String {
51− self.wrap("92", s)
48+ self.wrap("32", s)
52 }
49 }
53 pub fn yellow(&self, s: &str) -> String {
50 pub fn yellow(&self, s: &str) -> String {
54− self.wrap("93", s)
51+ self.wrap("33", s)
55− }
56− fn id(&self, s: &str) -> String {
57− self.wrap("1;95", s)
58− }
59− fn label(&self, s: &str) -> String {
60− self.wrap("1;92", s)
61− }
62− fn field(&self, s: &str) -> String {
63− self.wrap("2;96", s)
64− }
65− fn rail(&self, lane: usize, s: &str) -> String {
66− let codes = ["95", "96", "93", "92"];
67− self.wrap(codes[lane.saturating_sub(1) % codes.len()], s)
68 }
52 }
69 pub fn grey(&self, bright: u8, s: &str) -> String {
53 pub fn grey(&self, bright: u8, s: &str) -> String {
70 // grey scale: brighter when more recent
54 // grey scale: brighter when more recent
71 self.wrap(&format!("38;5;{}", 245 + bright.min(10)), s)
55 self.wrap(&format!("38;5;{}", 245 + bright.min(10)), s)
72 }
56 }
73 pub fn accent(&self, level: u8, s: &str) -> String {
57 pub fn accent(&self, level: u8, s: &str) -> String {
74− // Size remains encoded by the bar's height, with a vivid cool-to-warm ramp.
58+ // size bar: one accent colour, brighter with size
75− let codes = ["96", "94", "95", "93", "1;93"];
59+ let codes = ["38;5;240", "38;5;36", "38;5;32", "38;5;39", "38;5;87"];
76 self.wrap(codes[level.min(4) as usize], s)
60 self.wrap(codes[level.min(4) as usize], s)
77 }
61 }
78 pub fn author(&self, name: &str, s: &str) -> String {
62 pub fn author(&self, name: &str, s: &str) -> String {
@@ -82,7 +66,7 @@ impl Palette {
82 h = h.wrapping_mul(33).wrapping_add(b as u32);
66 h = h.wrapping_mul(33).wrapping_add(b as u32);
83 }
67 }
84 let hue = (h % 6) as u8;
68 let hue = (h % 6) as u8;
85− let code = ["91", "92", "93", "94", "95", "96"][hue as usize];
69+ let code = ["31", "32", "33", "34", "35", "36"][hue as usize];
86 self.wrap(code, s)
70 self.wrap(code, s)
87 }
71 }
88 /// colour with a raw ANSI code (used for meaning-coloured glyphs)
72 /// colour with a raw ANSI code (used for meaning-coloured glyphs)
@@ -114,8 +98,7 @@ pub fn color_enabled(mode: &str) -> bool {
114 match mode {
98 match mode {
115 "always" => true,
99 "always" => true,
116 "never" => false,
100 "never" => false,
117− _ => std::env::var_os("TERM").map(|t| t != "dumb").unwrap_or(true)
118− && crate::show::stdout_is_tty(),
101+ _ => crate::show::stdout_is_tty(),
119 }
102 }
120}
103}
121
104
@@ -171,16 +154,11 @@ fn field_set(v: &Value) -> Option<BTreeSet<String>> {
171
154
172fn shape_of_record(interp: &Interp, v: &Value) -> Option<String> {
155fn shape_of_record(interp: &Interp, v: &Value) -> Option<String> {
173 let fs = field_set(v)?;
156 let fs = field_set(v)?;
174− // Only these named shapes have special display forms (§5.1). Searching
157+ for (name, _) in &interp.shapes.decls {
175− // every declaration let a custom alias win in HashMap iteration order,
176− // randomly turning Commit, Entry, etc. values into generic records.
177− // Resolve each configured shape so aliases and custom field sets still
178− // determine which records match.
179− for name in ["Commit", "Entry", "Subtree", "Repo", "Frame", "Change"] {
180 if let Some(s) = interp.shapes.shape_of(name) {
158 if let Some(s) = interp.shapes.shape_of(name) {
181 if let ShapeKind::Record(want) = &s.kind {
159 if let ShapeKind::Record(want) = &s.kind {
182 if want == &fs {
160 if want == &fs {
183− return Some(name.to_string());
161+ return Some(name.clone());
184 }
162 }
185 }
163 }
186 }
164 }
@@ -197,89 +175,35 @@ pub fn is_commit(interp: &Interp, v: &Value) -> bool {
197// ----------------------------------------------------------------------
175// ----------------------------------------------------------------------
198
176
199pub fn display(interp: &mut Interp, v: &Value, color: bool) -> Result<String, Crash> {
177pub fn display(interp: &mut Interp, v: &Value, color: bool) -> Result<String, Crash> {
200− display_with_width(interp, v, color, tty_width())
201−}
202−
203−/// Display column tables as if stdout had this width; `None` preserves full cells.
204−pub fn display_with_width(
205− interp: &mut Interp,
206− v: &Value,
207− color: bool,
208− term_w: Option<usize>,
209−) -> Result<String, Crash> {
210 let pal = Palette { on: color };
178 let pal = Palette { on: color };
211 let mut out = String::new();
179 let mut out = String::new();
212− let mut loaded = None;
213− let mut todo = vec![Block::Of(v.clone(), 0)];
214− while let Some(block) = todo.pop() {
215− match block {
216− Block::Of(v, indent) => {
217− display_block(interp, &v, &pal, indent, &mut out, &mut loaded, &mut todo, term_w)?
180+ display_block(interp, v, &pal, 0, &mut out)?;
218− }
219− Block::Items { xs, next, indent } => {
220− if let Some(x) = xs.get(next).cloned() {
221− // separated by a blank line
222− if next > 0 {
223− out.push('\n');
224− }
225− let next = next + 1;
226− todo.push(Block::Items { xs, next, indent });
227− todo.push(Block::Of(x, indent));
228− }
229− }
230− Block::Fields { m, after, name_w, indent } => display_fields(
231− interp,
232− &m,
233− after.as_deref(),
234− name_w,
235− &pal,
236− indent,
237− &mut out,
238− &mut loaded,
239− &mut todo,
240− )?,
241− }
242− }
243 if !out.ends_with('\n') {
181 if !out.ends_with('\n') {
244 out.push('\n');
182 out.push('\n');
245 }
183 }
246 Ok(out)
184 Ok(out)
247}
185}
248
186
249−/// A block still to be displayed, or the rest of one that holds blocks
250−/// (§5.1): a list shown one block per item, and a generic record, whose list
251−/// fields print in block form under their key. Blocks nest as deep as the
252−/// value's lists do, so `display` keeps them on a stack of its own instead of
253−/// recursing: at about 14 KB of native stack a level in a debug build, a list
254−/// nested as deep as the parser allows (§3.4) overflowed the worker's 512 MB,
255−/// and one built at run time is not bounded at all.
256−enum Block {
257− /// a value in block form, at an indent
258− Of(Value, usize),
259− /// a list's items from the `next`th on, one block each
260− Items { xs: ListVal, next: usize, indent: usize },
261− /// a generic record's fields after the field `after`, or all of them
262− Fields { m: Rc<RecordMap>, after: Option<String>, name_w: usize, indent: usize },
263−}
264−
265fn display_id(interp: &Interp, id: &str, pal: &Palette) -> String {
187fn display_id(interp: &Interp, id: &str, pal: &Palette) -> String {
266− let p = interp.unique_prefix(id);
188+ let p = interp.backend.unique_prefix(id);
267 let n = p.len();
189 let n = p.len();
268− format!("{}{}", pal.id(p.as_str()), pal.dim(&id[n..]))
190+ format!("{}{}", pal.bold(p.as_str()), pal.dim(&id[n..]))
269}
191}
270
192
271−fn display_line(
272− interp: &mut Interp,
273− v: &Value,
274− pal: &Palette,
275− loaded: &mut Option<Loaded>,
276−) -> Result<String, Crash> {
193+fn display_line(interp: &Interp, v: &Value, pal: &Palette) -> Result<String, Crash> {
277 let v = &v.forced()?;
194 let v = &v.forced()?;
278 match v {
195 match v {
279 Value::Thunk(_) => unreachable!("forced never returns a thunk"),
196 Value::Thunk(_) => unreachable!("forced never returns a thunk"),
280− Value::Int(n) => Ok(pal.yellow(&n.to_string())),
197+ Value::Int(n) => Ok(n.to_string()),
281− Value::Bool(b) => Ok(if *b { pal.green("true") } else { pal.red("false") }),
198+ Value::Bool(b) => Ok(b.to_string()),
282− Value::Text(t) => Ok(first_line(t)),
199+ Value::Text(t) => {
200+ let first = t.lines().next().unwrap_or("");
201+ if t.lines().count() > 1 {
202+ Ok(format!("{}…", first))
203+ } else {
204+ Ok(first.to_string())
205+ }
206+ }
283 Value::Id(id) => Ok(display_id(interp, id, pal)),
207 Value::Id(id) => Ok(display_id(interp, id, pal)),
284 Value::Blob(b) => {
208 Value::Blob(b) => {
285 let size = human_size(b.size());
209 let size = human_size(b.size());
@@ -289,19 +213,43 @@ fn display_line(
289 Ok(format!("‹{}›", size))
213 Ok(format!("‹{}›", size))
290 }
214 }
291 }
215 }
292− // a lambda renders as its source (§5.2), which may run over several
293− // lines and hold comments; the line form keeps only the first
294− Value::Fun(_) => Ok(first_line(&crate::show::show(interp, v)?)),
216+ Value::Fun(f) => Ok(crate::show::show(&Interp::dummy(), &Value::Fun(f.clone()))),
295− Value::Shape(s) => Ok(pal.cyan(&s.name)),
217+ Value::Shape(s) => Ok(s.name.clone()),
296 Value::Record(_) => {
218 Value::Record(_) => {
297 if let Some(shape) = shape_of_record(interp, v) {
219 if let Some(shape) = shape_of_record(interp, v) {
298 match shape.as_str() {
220 match shape.as_str() {
299− "Commit" => return line_in(interp, v, pal, loaded, false),
221+ "Commit" => return commit_line(interp, v, pal, false, &BTreeSet::new(), false),
300 "Entry" => return entry_line(interp, v, pal),
222 "Entry" => return entry_line(interp, v, pal),
301− "Subtree" => return line_in(interp, &v.field("root")?, pal, loaded, false),
223+ "Subtree" => {
302− // its focus's line, drawn as the focus
224+ return commit_line(
303− "Repo" => return line_in(interp, &v.field("root")?, pal, loaded, true),
225+ interp,
304− "Frame" => return line_in(interp, &v.field("parent")?, pal, loaded, false),
226+ &v.field("root")?,
227+ pal,
228+ false,
229+ &BTreeSet::new(),
230+ false,
231+ )
232+ }
233+ "Repo" => {
234+ return commit_line(
235+ interp,
236+ &v.field("root")?,
237+ pal,
238+ true,
239+ &BTreeSet::new(),
240+ false,
241+ )
242+ }
243+ "Frame" => {
244+ return commit_line(
245+ interp,
246+ &v.field("parent")?,
247+ pal,
248+ false,
249+ &BTreeSet::new(),
250+ false,
251+ )
252+ }
305 "Change" => {
253 "Change" => {
306 let n = touched_paths(v)?.len();
254 let n = touched_paths(v)?.len();
307 return Ok(format!("{} paths", n));
255 return Ok(format!("{} paths", n));
@@ -316,18 +264,6 @@ fn display_line(
316 }
264 }
317}
265}
318
266
319−/// The line form of text that may run over several lines (§5.1): its first
320−/// line, `…` if there are more.
321−fn first_line(s: &str) -> String {
322− let mut lines = s.lines();
323− let first = lines.next().unwrap_or("");
324− if lines.next().is_some() {
325− format!("{}…", first)
326− } else {
327− first.to_string()
328− }
329−}
330−
331fn list_line(_interp: &Interp, xs: &[Value], pal: &Palette) -> Result<String, Crash> {
267fn list_line(_interp: &Interp, xs: &[Value], pal: &Palette) -> Result<String, Crash> {
332 if xs.is_empty() {
268 if xs.is_empty() {
333 return Ok("0 items".into());
269 return Ok("0 items".into());
@@ -364,13 +300,14 @@ fn list_line(_interp: &Interp, xs: &[Value], pal: &Palette) -> Result<String, Cr
364 let parts: Vec<String> = xs
300 let parts: Vec<String> = xs
365 .iter()
301 .iter()
366 .map(|x| match x {
302 .map(|x| match x {
367− Value::Int(n) => pal.yellow(&n.to_string()),
303+ Value::Int(n) => n.to_string(),
368− Value::Bool(b) => if *b { pal.green("true") } else { pal.red("false") },
304+ Value::Bool(b) => b.to_string(),
369 _ => unreachable!(),
305 _ => unreachable!(),
370 })
306 })
371 .collect();
307 .collect();
372 return Ok(parts.join(", "));
308 return Ok(parts.join(", "));
373 }
309 }
310+ let _ = pal;
374 Ok(format!("{} items", xs.len()))
311 Ok(format!("{} items", xs.len()))
375}
312}
376
313
@@ -383,9 +320,9 @@ fn entry_line(interp: &Interp, v: &Value, pal: &Palette) -> Result<String, Crash
383 };
320 };
384 let _ = interp;
321 let _ = interp;
385 if unresolved {
322 if unresolved {
386− Ok(format!("{} {} {}", pal.red("✖"), pal.cyan(&path), pal.red(&size)))
323+ Ok(format!("{} {} {}", pal.red("✖"), path, size))
387 } else {
324 } else {
388− Ok(format!(" {} {}", pal.cyan(&path), pal.yellow(&size)))
325+ Ok(format!(" {} {}", path, size))
389 }
326 }
390}
327}
391
328
@@ -455,323 +392,6 @@ fn touched_in_maps(from: &SnapMap, to: &SnapMap) -> Result<Vec<(Vec<String>, cha
455 Ok(out)
392 Ok(out)
456}
393}
457
394
458−/// The line count from which the size bar draws its last glyph, `▇` (§7.11
459−/// column 5): no count past it changes the bar, so none is computed.
460−const SIZE_BAR_TOP: usize = 1000;
461−
462−/// Lines added plus removed between two versions of one path (§7.11 size
463−/// bar), either possibly absent, counted up to `cap`: the length of a
464−/// shortest line diff, or `cap` if that is at least `cap`. Lines are split
465−/// after each `\n` as `line_count` counts them. A conflict counts as its
466−/// materialized text, as it is checked out.
467−///
468−/// A line diff costs the product of the file's length and the size of the
469−/// change, so a plain one of a rewritten or reordered long file takes
470−/// seconds. The search here stops at the cap rather than at a clock, so the
471−/// same two versions always give the same count. Every step before it keeps
472−/// the count exact: the common prefix and suffix are equal lines of some
473−/// shortest diff, and the difference in length is a lower bound. A small
474−/// change is then settled by a short search over the lines as they are.
475−/// Otherwise a line that does not occur on the other side at all cannot be
476−/// part of any common subsequence, so, as git's xdiff does, it counts as
477−/// changed up front, and the rest is searched unless `reorder_bound` already
478−/// puts it past the cap. That search (`line_distance`) costs at most about
479−/// twice what a bit-parallel search of `cap / 64 + 2` machine words per line
480−/// does, whatever the lines are: Myers' search soon stops on most content,
481−/// but on a long file of a few lines repeating it can make a pass over the
482−/// lines for each of the `2 × cap + 1` diagonals it reaches, so once it has
483−/// cost half that the bit-parallel search counts instead. A million lines of
484−/// two alternating, with 480 pairs of neighbours swapped, took Myers' search
485−/// alone some 5 × 10⁸ comparisons.
486−fn changed_lines(from: Option<&Value>, to: Option<&Value>, cap: usize) -> Result<usize, Crash> {
487− use std::collections::HashMap;
488− // the rounds of the first, short search
489− const QUICK: usize = 64;
490− let text = |v: Option<&Value>| -> Result<Vec<u8>, Crash> {
491− match v {
492− Some(Value::Blob(b)) => b.bytes(),
493− _ => Ok(Vec::new()),
494− }
495− };
496− let (a, b) = (text(from)?, text(to)?);
497− let a: Vec<&[u8]> = a.split_inclusive(|c| *c == b'\n').collect();
498− let b: Vec<&[u8]> = b.split_inclusive(|c| *c == b'\n').collect();
499− let (a, b) = trim_common(&a, &b);
500− if a.len().abs_diff(b.len()) >= cap {
501− return Ok(cap);
502− }
503− // the short search's few rounds bound what it costs, so no step limit
504− let quick = cap.min(QUICK);
505− match edit_distance(a, b, quick, usize::MAX, &mut 0) {
506− Some(d) if d < quick || quick == cap => return Ok(d),
507− _ => {}
508− }
509− // number each distinct line, so the rest compares numbers; the lines
510− // numbered while reading `a` are those that occur in it
511− fn number<'l>(ids: &mut HashMap<&'l [u8], usize>, lines: &[&'l [u8]]) -> Vec<usize> {
512− lines
513− .iter()
514− .map(|l| {
515− let next = ids.len();
516− *ids.entry(*l).or_insert(next)
517− })
518− .collect()
519− }
520− let mut ids = HashMap::new();
521− let a = number(&mut ids, a);
522− let in_a = ids.len();
523− let b = number(&mut ids, b);
524− let mut in_b = vec![false; ids.len()];
525− for l in &b {
526− in_b[*l] = true;
527− }
528− let a_common: Vec<usize> = a.iter().copied().filter(|l| in_b[*l]).collect();
529− let b_common: Vec<usize> = b.iter().copied().filter(|l| *l < in_a).collect();
530− let unmatched = (a.len() - a_common.len()) + (b.len() - b_common.len());
531− if unmatched >= cap {
532− return Ok(cap);
533− }
534− let (a, b) = trim_common(&a_common, &b_common);
535− let max = cap - unmatched;
536− if reorder_bound(a, b, ids.len()) >= max {
537− return Ok(cap);
538− }
539− Ok(unmatched + line_distance(a, b, ids.len(), max, &mut 0))
540−}
541−
542−/// `a` and `b` without the lines they start and end with in common.
543−fn trim_common<'s, T: PartialEq>(a: &'s [T], b: &'s [T]) -> (&'s [T], &'s [T]) {
544− let pre = a.iter().zip(b).take_while(|(x, y)| x == y).count();
545− let (a, b) = (&a[pre..], &b[pre..]);
546− let suf = a.iter().rev().zip(b.iter().rev()).take_while(|(x, y)| x == y).count();
547− (&a[..a.len() - suf], &b[..b.len() - suf])
548−}
549−
550−/// The number of lines deleted plus inserted by a shortest diff of `a` to
551−/// `b`, or `max` if that is at least `max`: Myers' greedy search, which
552−/// reaches the end on round `d` exactly when the shortest diff has `d` steps,
553−/// run for `max` rounds at most. A round extends each of its diagonals along
554−/// equal lines from where the round before left off, so the work is at most
555−/// `max` squared diagonals plus one pass over the lines per diagonal. Each
556−/// pair of lines compared along a diagonal adds a step to `steps`, and each
557−/// diagonal reached `DIAGONAL` steps; `None` once they pass `limit`.
558−fn edit_distance<T: PartialEq>(
559− a: &[T],
560− b: &[T],
561− max: usize,
562− limit: usize,
563− steps: &mut usize,
564−) -> Option<usize> {
565− let (n, m) = (a.len() as isize, b.len() as isize);
566− // the furthest `x` reached on diagonal `k = x - y`, at `v[k + max]`;
567− // round `d` writes diagonals `-d..=d` from those round `d - 1` wrote, and
568− // round 0 starts from the zero on diagonal 1
569− let mut v = vec![0isize; 2 * max + 1];
570− let at = |k: isize| (k + max as isize) as usize;
571− for d in 0..max as isize {
572− for k in (-d..=d).step_by(2) {
573− // from diagonal `k + 1` by inserting a line of `b`, or from
574− // `k - 1` by deleting one of `a`, whichever gets further
575− let from = if k == -d || (k != d && v[at(k - 1)] < v[at(k + 1)]) {
576− v[at(k + 1)]
577− } else {
578− v[at(k - 1)] + 1
579− };
580− let (mut x, mut y) = (from, from - k);
581− while x < n && y < m && a[x as usize] == b[y as usize] {
582− x += 1;
583− y += 1;
584− }
585− *steps += DIAGONAL + (x - from) as usize;
586− if *steps > limit {
587− return None;
588− }
589− v[at(k)] = x;
590− if x >= n && y >= m {
591− return Some(d as usize);
592− }
593− }
594− }
595− Some(max)
596−}
597−
598−/// What reaching a diagonal costs `edit_distance`, in comparisons of two
599−/// lines along one, the unit both searches count their steps in: measured at
600−/// about twelve in a debug build and seven in release.
601−const DIAGONAL: usize = 8;
602−
603−/// What a word costs `banded_distance`, in the same comparisons: measured at
604−/// about six in a debug build and three to four in release. Weighted so,
605−/// `line_distance` gives up Myers' search once it has cost about what the
606−/// other search will; counted alike, it gave it up on a long file of a few
607−/// lines repeating where finishing would have taken half the time or less.
608−const WORD: usize = 4;
609−
610−/// `edit_distance` of lines numbered below `ids`, in at most twice the steps
611−/// `banded_distance` takes and one diagonal with its run, whatever the lines
612−/// are. Myers' search soon stops on most content, but on a long file of a
613−/// few lines repeating it runs along equal lines on many diagonals, a pass
614−/// over the file for each, so it may take only as many steps as
615−/// `banded_distance` takes at most, and that counts instead if it has not
616−/// finished by then. Steps are weighted by what they cost (`DIAGONAL`,
617−/// `WORD`), so that is when it has cost about as much as the other search
618−/// will. Both find the same count; `steps` adds up the steps of both.
619−fn line_distance(a: &[usize], b: &[usize], ids: usize, max: usize, steps: &mut usize) -> usize {
620− let limit = *steps + banded_steps(a.len(), b.len(), max);
621− match edit_distance(a, b, max, limit, steps) {
622− Some(d) => d,
623− None => banded_distance(a, b, ids, max, steps),
624− }
625−}
626−
627−/// The diagonals `x - y` from `-ins` to `del` that a diff of `n` lines to
628−/// `m` stays on, keeping line `x` of the one with line `y` of the other, when
629−/// it deletes plus inserts fewer than `max` lines, or `None` if none does:
630−/// it deletes `n - m` more lines than it inserts, so at most `del` and
631−/// inserts at most `ins`.
632−fn band(n: usize, m: usize, max: usize) -> Option<(usize, usize)> {
633− (n.abs_diff(m) < max).then(|| ((max - 1 + n - m) / 2, (max - 1 + m - n) / 2))
634−}
635−
636−/// The most steps `banded_distance` takes on `n` lines and `m` under `max`:
637−/// `WORD` per line of the first, to find where its lines are, and per word
638−/// of it the band crosses, at most `(del + ins) / 64 + 2`, per line of the
639−/// second.
640−fn banded_steps(n: usize, m: usize, max: usize) -> usize {
641− match band(n, m, max) {
642− Some((del, ins)) if n > 0 && m > 0 => WORD * (n + m * ((del + ins) / 64 + 2)),
643− _ => 0,
644− }
645−}
646−
647−/// `edit_distance` of lines numbered below `ids`, found a machine word of `a`
648−/// at a time for each line of `b` (Hyyrö's bit-parallel longest common
649−/// subsequence), over only the band of diagonals a diff counting less than
650−/// `max` stays on, as Ukkonen bounds the search. Its steps, which it adds to
651−/// `steps`, are `WORD` per line of `a` and per word of `a` the band crosses
652−/// per line of `b`, whatever the lines are.
653−///
654−/// After each line of `b`, the zero bits of `a`'s words up to a place in
655−/// `a` count a longest common subsequence of `a` up to there and `b` so far.
656−/// Only the words the band crosses change: one above the band keeps what it
657−/// last had, and one below it still has the ones it started with, as if the
658−/// length grew no further down. Neither counts more than a common
659−/// subsequence has, and inside the band the counts are exact whenever a diff
660−/// of fewer than `max` lines exists, since such a diff keeps inside the band;
661−/// otherwise the count is `max` whatever they are.
662−fn banded_distance(a: &[usize], b: &[usize], ids: usize, max: usize, steps: &mut usize) -> usize {
663− let (n, m) = (a.len(), b.len());
664− let Some((del, ins)) = band(n, m, max) else {
665− return max;
666− };
667− if n == 0 || m == 0 {
668− return n + m;
669− }
670− *steps += WORD * n;
671− // per line, from `start[l]` to `start[l + 1]`: the words of `a` it occurs
672− // in, in order, with a bit where it occurs in each
673− let mut start = vec![0usize; ids + 1];
674− let mut last = vec![usize::MAX; ids];
675− for (i, l) in a.iter().enumerate() {
676− if last[*l] != i / 64 {
677− last[*l] = i / 64;
678− start[*l + 1] += 1;
679− }
680− }
681− drop(last);
682− for l in 0..ids {
683− start[l + 1] += start[l];
684− }
685− let (mut word, mut bits) = (vec![0usize; start[ids]], vec![0u64; start[ids]]);
686− let mut next = start[..ids].to_vec();
687− for (i, l) in a.iter().enumerate() {
688− let e = next[*l];
689− if e == start[*l] || word[e - 1] != i / 64 {
690− word[e] = i / 64;
691− next[*l] += 1;
692− }
693− bits[next[*l] - 1] |= 1 << (i % 64);
694− }
695− // before any line of `b` nothing is common; from here on `next[l]` is the
696− // first word line `l` occurs in that is not above the band
697− let mut v = vec![u64::MAX; n.div_ceil(64)];
698− next.copy_from_slice(&start[..ids]);
699− // as slices, which a debug build indexes without a call
700− let (v, next) = (&mut v[..], &mut next[..]);
701− let (start, word, bits) = (&start[..], &word[..], &bits[..]);
702− for (j, l) in b.iter().enumerate() {
703− // line `j` of `b` can only be kept with lines `j - ins` to `j + del`
704− // of `a`
705− let (lo, hi) = (j.saturating_sub(ins) / 64, (j + del).min(n - 1) / 64);
706− *steps += WORD * (hi + 1 - lo);
707− let end = start[*l + 1];
708− let mut e = next[*l];
709− while e < end && word[e] < lo {
710− e += 1;
711− }
712− next[*l] = e;
713− // the words above the band do not change, so nothing carries in
714− let mut carry = false;
715− for (w, v) in v[lo..=hi].iter_mut().enumerate() {
716− let here = if e < end && word[e] == lo + w {
717− e += 1;
718− bits[e - 1]
719− } else {
720− 0
721− };
722− let x = *v;
723− let (sum, c1) = x.overflowing_add(x & here);
724− let (sum, c2) = sum.overflowing_add(carry as u64);
725− carry = c1 || c2;
726− *v = sum | (x & !here);
727− }
728− }
729− let common: usize = v.iter().map(|w| w.count_zeros() as usize).sum();
730− (n + m - 2 * common).min(max)
731−}
732−
733−/// A lower bound on the lines deleted plus inserted by any diff of `a` to
734−/// `b` (lines numbered below `ids`), cheap where the search is dearest: a
735−/// long file whose lines were reordered, or one of a few lines repeating
736−/// that now occur a different number of times. A line that occurs once on
737−/// each side can only be kept by pairing those two, and the pairs kept keep
738−/// their order on both sides, so at most the longest sequence of them in the
739−/// same order on both sides is kept (patience sorting finds its length); any
740−/// other line is kept at most as many times as it occurs on the side where
741−/// it occurs fewer times.
742−fn reorder_bound(a: &[usize], b: &[usize], ids: usize) -> usize {
743− // per line: its occurrences in `a` and in `b`, and where it is in `a`
744− let mut seen = vec![(0usize, 0usize, 0usize); ids];
745− for (i, l) in a.iter().enumerate() {
746− seen[*l].0 += 1;
747− seen[*l].2 = i;
748− }
749− for l in b {
750− seen[*l].1 += 1;
751− }
752− // over the lines once on each side, in `b`'s order, by where they are in
753− // `a`: `ends[i]` is the least last place of an increasing sequence of
754− // `i + 1` of them
755− let mut ends: Vec<usize> = Vec::new();
756− for l in b {
757− if let (1, 1, i) = seen[*l] {
758− let at = ends.partition_point(|e| *e < i);
759− if at == ends.len() {
760− ends.push(i);
761− } else {
762− ends[at] = i;
763− }
764− }
765− }
766− let others: usize = seen
767− .iter()
768− .filter(|(in_a, in_b, _)| (*in_a, *in_b) != (1, 1))
769− .map(|(in_a, in_b, _)| in_a.min(in_b))
770− .sum();
771− let kept = ends.len() + others;
772− a.len() + b.len() - 2 * kept
773−}
774−
775fn snapshot_map_of(snap: &Value) -> Result<SnapMap, Crash> {
395fn snapshot_map_of(snap: &Value) -> Result<SnapMap, Crash> {
776 let mut m = BTreeMap::new();
396 let mut m = BTreeMap::new();
777 for e in snap.as_list()?.iter() {
397 for e in snap.as_list()?.iter() {
@@ -786,24 +406,13 @@ fn snapshot_map_of(snap: &Value) -> Result<SnapMap, Crash> {
786 Ok(m)
406 Ok(m)
787}
407}
788
408
789−/// What a commit's glyph says about where it stands in a history
790−/// (specs/tree.md §Glyphs), beyond what its own files say. A `Commit` value
791−/// records none of it, so both its forms read it from the repository the
792−/// expression was given (`Loaded`).
793−struct Standing {
794− focus: bool,
795− /// an ancestor of the focus
796− ancestor: bool,
797− immutable: bool,
798− /// no change against its parent
799− empty: bool,
800−}
801−
802fn commit_line(
409fn commit_line(
803 interp: &Interp,
410 interp: &Interp,
804 c: &Value,
411 c: &Value,
805 pal: &Palette,
412 pal: &Palette,
806− standing: &Standing,
413+ focus: bool,
414+ immutable: &BTreeSet<String>,
415+ has_conflicts_hint: bool,
807) -> Result<String, Crash> {
416) -> Result<String, Crash> {
808 let id = match c.field("id")? {
417 let id = match c.field("id")? {
809 Value::Id(i) => i.to_string(),
418 Value::Id(i) => i.to_string(),
@@ -817,20 +426,14 @@ fn commit_line(
817 .iter()
426 .iter()
818 .map(|l| l.as_text().map(|s| s.to_string()))
427 .map(|l| l.as_text().map(|s| s.to_string()))
819 .collect::<Result<_, _>>()?;
428 .collect::<Result<_, _>>()?;
820− let glyph = node_glyph(c, &id, standing)?;
429+ let glyph = node_glyph(interp, c, &id, focus, immutable, pal)?;
821− let id_s = if glyph == "⊗" {
822− pal.red(&display_id(interp, &id, &Palette { on: false }))
430+ let id_s = display_id(interp, &id, pal);
823− } else if standing.immutable {
824− pal.blue(&display_id(interp, &id, &Palette { on: false }))
825− } else {
826− display_id(interp, &id, pal)
827− };
828− let glyph = pal.code(node_ansi(&glyph), &glyph);
829 let labels_s = if labels.is_empty() {
431 let labels_s = if labels.is_empty() {
830 String::new()
432 String::new()
831 } else {
433 } else {
832− pal.label(&labels.join(" "))
434+ pal.green(&labels.join(" "))
833 };
435 };
436+ let _ = has_conflicts_hint;
834 Ok(if labels_s.is_empty() {
437 Ok(if labels_s.is_empty() {
835 format!("{} {} {}", glyph, id_s, msg_first)
438 format!("{} {} {}", glyph, id_s, msg_first)
836 } else {
439 } else {
@@ -838,23 +441,28 @@ fn commit_line(
838 })
441 })
839}
442}
840
443
841−/// The glyph of a commit line, by `tree`'s precedence (`glyph_for`).
444+fn node_glyph(
445+ interp: &Interp,
446+ c: &Value,
447+ id: &str,
448+ focus: bool,
449+ immutable: &BTreeSet<String>,
450+ pal: &Palette,
842−fn node_glyph(c: &Value, id: &str, standing: &Standing) -> Result<String, Crash> {
451+) -> Result<String, Crash> {
843 let g = if id == ROOT_ID {
452 let g = if id == ROOT_ID {
844 "⌂"
453 "⌂"
845 } else if has_conflict(c)? {
454 } else if has_conflict(c)? {
846 "⊗"
455 "⊗"
847− } else if standing.empty {
456+ } else if is_empty_commit(interp, c)? {
848 "◌"
457 "◌"
849− } else if standing.immutable {
458+ } else if immutable.contains(id) {
850 "◆"
459 "◆"
851− } else if standing.focus {
460+ } else if focus {
852 "◉"
461 "◉"
853− } else if standing.ancestor {
854− "●"
855 } else {
462 } else {
856 "○"
463 "○"
857 };
464 };
465+ let _ = pal;
858 Ok(g.to_string())
466 Ok(g.to_string())
859}
467}
860
468
@@ -870,17 +478,18 @@ fn has_conflict(c: &Value) -> Result<bool, Crash> {
870 Ok(false)
478 Ok(false)
871}
479}
872
480
873−/// `v` in block form, the blocks it holds left on `todo` (see `Block`)
481+/// empty: no change against its parent — needs the parent; we only compute
874−#[allow(clippy::too_many_arguments)]
482+/// this meaningfully in tree rendering. In line display, treat as false.
483+fn is_empty_commit(_interp: &Interp, _c: &Value) -> Result<bool, Crash> {
484+ Ok(false)
485+}
486+
875fn display_block(
487fn display_block(
876 interp: &mut Interp,
488 interp: &mut Interp,
877 v: &Value,
489 v: &Value,
878 pal: &Palette,
490 pal: &Palette,
879 indent: usize,
491 indent: usize,
880 out: &mut String,
492 out: &mut String,
881− loaded: &mut Option<Loaded>,
882− todo: &mut Vec<Block>,
883− term_w: Option<usize>,
884) -> Result<(), Crash> {
493) -> Result<(), Crash> {
885 let pad = " ".repeat(indent);
494 let pad = " ".repeat(indent);
886 let v = &v.forced()?;
495 let v = &v.forced()?;
@@ -895,31 +504,18 @@ fn display_block(
895 out.push_str(&indent_multiline(&content, indent));
504 out.push_str(&indent_multiline(&content, indent));
896 return Ok(());
505 return Ok(());
897 }
506 }
898− Value::Int(_) | Value::Bool(_) | Value::Id(_) | Value::Shape(_) => {
507+ Value::Int(_) | Value::Bool(_) | Value::Id(_) | Value::Fun(_) | Value::Shape(_) => {
899 out.push_str(&pad);
508 out.push_str(&pad);
900− out.push_str(&display_line(interp, v, pal, loaded)?);
509+ out.push_str(&display_line(interp, v, pal)?);
901 out.push('\n');
510 out.push('\n');
902 return Ok(());
511 return Ok(());
903 }
512 }
904− // all of a lambda's source, of which the line form keeps the first
905− // line (§5.1), each line under the block's indent
906− Value::Fun(_) => {
907− for l in crate::show::show(interp, v)?.lines() {
908− out.push_str(&pad);
909− out.push_str(l);
910− out.push('\n');
911− }
912− return Ok(());
913− }
914− Value::List(xs) => return display_list_block(interp, xs, pal, indent, out, loaded, todo, term_w),
513+ Value::List(xs) => return display_list_block(interp, xs, pal, indent, out),
915 Value::Record(_) => {}
514 Value::Record(_) => {}
916 }
515 }
917 if let Some(shape) = shape_of_record(interp, v) {
516 if let Some(shape) = shape_of_record(interp, v) {
918 match shape.as_str() {
517 match shape.as_str() {
919− "Commit" => {
920− let loaded = Loaded::cached(interp, loaded)?;
921− return display_commit_block(interp, v, pal, indent, out, loaded);
518+ "Commit" => return display_commit_block(interp, v, pal, indent, out),
922− }
923 "Entry" => {
519 "Entry" => {
924 out.push_str(&pad);
520 out.push_str(&pad);
925 out.push_str(&entry_line(interp, v, pal)?);
521 out.push_str(&entry_line(interp, v, pal)?);
@@ -933,7 +529,7 @@ fn display_block(
933 ("children", v.field("children")?),
529 ("children", v.field("children")?),
934 ("context", Value::list(vec![])),
530 ("context", Value::list(vec![])),
935 ]);
531 ]);
936− let text = tree_render(interp, &opts, &repo, pal, false, term_w)?;
532+ let text = tree_render(interp, &opts, &repo, pal, false)?;
937 out.push_str(&indent_multiline(&text, indent));
533 out.push_str(&indent_multiline(&text, indent));
938 return Ok(());
534 return Ok(());
939 }
535 }
@@ -949,14 +545,30 @@ fn display_block(
949 }
545 }
950 "Frame" => {
546 "Frame" => {
951 out.push_str(&pad);
547 out.push_str(&pad);
952− out.push_str(&line_in(interp, &v.field("parent")?, pal, loaded, false)?);
548+ out.push_str(&commit_line(
549+ interp,
550+ &v.field("parent")?,
551+ pal,
552+ false,
553+ &BTreeSet::new(),
554+ false,
555+ )?);
953 out.push('\n');
556 out.push('\n');
954 return Ok(());
557 return Ok(());
955 }
558 }
956 "Change" => {
559 "Change" => {
957− for (p, mark, _) in touched_paths(v)? {
560+ for (p, mark, unresolved) in touched_paths(v)? {
958 out.push_str(&pad);
561 out.push_str(&pad);
562+ let ms = mark.to_string();
563+ let colored = match mark {
564+ '+' => pal.green(&ms),
565+ '−' => pal.red(&ms),
566+ '~' => pal.yellow(&ms),
567+ '✖' => pal.red(&ms),
568+ _ => ms,
569+ };
570+ let _ = unresolved;
959− out.push_str(&format!("{} {}\n", colored_mark(mark, pal), p));
571+ out.push_str(&format!("{} {}\n", colored, p));
960 }
572 }
961 return Ok(());
573 return Ok(());
962 }
574 }
@@ -969,68 +581,19 @@ fn display_block(
969 _ => unreachable!(),
581 _ => unreachable!(),
970 };
582 };
971 let name_w = m.keys().map(|k| width(k)).max().unwrap_or(0);
583 let name_w = m.keys().map(|k| width(k)).max().unwrap_or(0);
972− display_fields(interp, m, None, name_w, pal, indent, out, loaded, todo)
584+ for (k, x) in m.iter() {
973−}
974−
975−/// A generic record's fields after the field `after`, or all of them, one
976−/// per line under names `name_w` wide, up to the first that is a non-empty
977−/// list: its block is left on `todo` to be displayed next, and the fields
978−/// after it to follow.
979−#[allow(clippy::too_many_arguments)]
980−fn display_fields(
981− interp: &mut Interp,
982− m: &Rc<RecordMap>,
983− after: Option<&str>,
984− name_w: usize,
985− pal: &Palette,
986− indent: usize,
987− out: &mut String,
988− loaded: &mut Option<Loaded>,
989− todo: &mut Vec<Block>,
990−) -> Result<(), Crash> {
991− let pad = " ".repeat(indent);
992− let status = (m.len() == 5
993− || (m.len() == 6 && matches!(m.get("unrecorded"), Some(Value::List(_)))))
994− && matches!(m.get("id"), Some(Value::Id(_)))
995− && matches!(m.get("message"), Some(Value::Text(_)))
996− && ["changed", "conflicts", "labels"].iter()
997− .all(|key| matches!(m.get(*key), Some(Value::List(_))));
998− let from = after.map_or(Bound::Unbounded, Bound::Excluded);
999− for (k, x) in m.range::<str, _>((from, Bound::Unbounded)) {
1000 out.push_str(&pad);
585 out.push_str(&pad);
1001− let label = if status && m.contains_key("unrecorded") && k == "changed" {
1002− "in commit"
1003− } else {
1004− k.as_str()
1005− };
1006− out.push_str(&pal.field(&pad_right(label, name_w)));
586+ out.push_str(&pal.dim(&pad_right(k, name_w)));
1007 out.push_str(" ");
587 out.push_str(" ");
1008− if status {
1009− let empty = match (k.as_str(), x) {
1010− ("changed", Value::List(xs)) if xs.is_empty() => Some("No changes"),
1011− ("unrecorded", Value::List(xs)) if xs.is_empty() => Some("All edits recorded"),
1012− ("conflicts", Value::List(xs)) if xs.is_empty() => Some("No conflicts"),
1013− ("labels", Value::List(xs)) if xs.is_empty() => Some("No labels"),
1014− ("message", Value::Text(s)) if s.is_empty() => Some("No description"),
1015− _ => None,
1016− };
1017− if let Some(text) = empty {
1018− out.push_str(&pal.dim(text));
1019− out.push('\n');
1020− continue;
1021− }
1022− }
1023 // lists print in block form under their key (§5.1)
588 // lists print in block form under their key (§5.1)
1024 if matches!(x, Value::List(xs) if !xs.is_empty()) {
589 if matches!(x, Value::List(xs) if !xs.is_empty()) {
1025 out.push('\n');
590 out.push('\n');
1026− let after = Some(k.clone());
591+ display_block(interp, x, pal, indent + name_w + 2, out)?;
1027− todo.push(Block::Fields { m: m.clone(), after, name_w, indent });
592+ } else {
1028− todo.push(Block::Of(x.clone(), indent + name_w + 2));
593+ let line = display_line(interp, x, pal)?;
1029− return Ok(());
594+ out.push_str(&line);
595+ out.push('\n');
1030 }
596 }
1031− let line = display_line(interp, x, pal, loaded)?;
1032− out.push_str(&line);
1033− out.push('\n');
1034 }
597 }
1035 Ok(())
598 Ok(())
1036}
599}
@@ -1047,204 +610,42 @@ fn indent_multiline(s: &str, indent: usize) -> String {
1047 + "\n"
610 + "\n"
1048}
611}
1049
612
1050−/// A change mark (`+ ~ − ✖`), coloured by what it means.
1051−fn colored_mark(mark: char, pal: &Palette) -> String {
1052− let ms = mark.to_string();
1053− match mark {
1054− '+' => pal.green(&ms),
1055− '−' | '✖' => pal.red(&ms),
1056− '~' => pal.yellow(&ms),
1057− _ => ms,
1058− }
1059−}
1060−
1061−/// The repository the expression was given (§1.2 step 4, after the
1062−/// snapshot), or, where only the loaded one is set (the interpreter tests),
1063−/// that one.
1064−fn given_repo(interp: &Interp) -> Option<Value> {
1065− let given = interp.given_repo.borrow().clone();
1066− given.or_else(|| interp.old_repo.borrow().clone())
1067−}
1068−
1069−/// The repository the expression was given (`given_repo`), indexed for
1070−/// displaying `Commit` values. A commit value records neither its parent nor
1071−/// where it stands, so both its forms read them from the commit with the
1072−/// same id there, as its block reads author and age (§5.1). Not the
1073−/// repository as loaded: the snapshot has rebased every descendant of the
1074−/// focus since, and the values displayed come from after it. Indexed once
1075−/// per displayed value, not once per commit: a table of commits would
1076−/// otherwise walk the history once for each row.
1077−struct Loaded {
1078− /// the parent of every commit that has one
1079− parents: BTreeMap<String, Value>,
1080− focus: String,
1081− /// the focus's ancestors, the focus excluded
1082− ancestors: BTreeSet<String>,
1083− immutable: BTreeSet<String>,
1084−}
1085−
1086−impl Loaded {
1087− /// `cache`, indexed when a display first draws a commit
1088− fn cached<'a>(interp: &mut Interp, cache: &'a mut Option<Loaded>) -> Result<&'a Loaded, Crash> {
1089− if cache.is_none() {
1090− *cache = Some(Loaded::of(interp)?);
1091− }
1092− Ok(cache.as_ref().expect("just set"))
1093− }
1094−
1095− fn of(interp: &mut Interp) -> Result<Loaded, Crash> {
1096− let mut loaded = Loaded {
1097− parents: BTreeMap::new(),
1098− focus: String::new(),
1099− ancestors: BTreeSet::new(),
1100− immutable: BTreeSet::new(),
1101− };
1102− let Some(repo) = given_repo(interp) else {
1103− return Ok(loaded);
1104− };
1105− if let Value::Id(i) = repo.field("root")?.field("id")? {
1106− loaded.focus = i.to_string();
1107− }
1108− for frame in repo.field("context")?.as_list()?.iter() {
1109− loaded.ancestors.insert(id_of_frame_parent(frame)?);
1110− }
1111− // as `tree_render` does: a config whose `immutable` crashes draws no `◆`
1112− loaded.immutable = crate::repo::compute_immutable(interp, &repo).unwrap_or_default();
1113− // every commit's parent, walking down from the top of the history
1114− let top = crate::repo::by_id(&repo, &top_id(&repo)?)?.unwrap_or_else(|| repo.clone());
1115− let mut todo = vec![(top.field("root")?, top.field("children")?)];
1116− while let Some((parent, children)) = todo.pop() {
1117− for c in children.as_list()?.iter() {
1118− let root = c.field("root")?;
1119− if let Value::Id(i) = root.field("id")? {
1120− loaded.parents.insert(i.to_string(), parent.clone());
1121− }
1122− todo.push((root, c.field("children")?));
1123− }
1124− }
1125− Ok(loaded)
1126− }
1127−
1128− /// Where the commit with id `id` stands here, given whether it changes
1129− /// nothing against its parent here.
1130− fn standing(&self, id: &str, empty: bool) -> Standing {
1131− Standing {
1132− focus: id == self.focus,
1133− ancestor: self.ancestors.contains(id),
1134− immutable: self.immutable.contains(id),
1135− empty,
1136− }
1137− }
1138−
1139− /// Where the commit value `c` stands here, for its line form: `◌` judged
1140− /// as the block judges it, by its files against its parent's here. While
1141− /// both still hold their stored trees the backend answers from tree ids,
1142− /// as for `tree` (`build_info`), so a table of commits reads no parent's
1143− /// files. A commit with no parent here claims no `◌`.
1144− fn line_standing(&self, interp: &Interp, c: &Value) -> Result<Standing, Crash> {
1145− let id = match c.field("id")? {
1146− Value::Id(i) => i.to_string(),
1147− _ => String::new(),
1148− };
1149− let empty = match self.parents.get(&id) {
1150− None => false,
1151− Some(parent) => {
1152− let parent_id = match parent.field("id")? {
1153− Value::Id(i) => i.to_string(),
1154− _ => String::new(),
1155− };
1156− let me = stored_origin(c).filter(|o| *o == id);
1157− let stored_parent = stored_origin(parent).filter(|o| *o == parent_id);
1158− let answer = match (me, stored_parent) {
1159− (Some(me), Some(parent)) => interp.backend.is_empty(me, parent),
1160− _ => None,
1161− };
1162− match answer {
1163− Some(e) => e,
1164− // in any order: a snapshot stands for a tree (§7.3)
1165− None => crate::repo::snapshot_eq(&c.field("files")?, &parent.field("files")?)?,
1166− }
1167− }
1168− };
1169− Ok(self.standing(&id, empty))
1170− }
1171−}
1172−
1173−/// A commit's line form, drawn where it stands in the repository the
1174−/// expression was given (§5.1); `as_focus` draws it as a `Repo`'s focus.
1175−fn line_in(
1176− interp: &mut Interp,
1177− c: &Value,
1178− pal: &Palette,
1179− loaded: &mut Option<Loaded>,
1180− as_focus: bool,
1181−) -> Result<String, Crash> {
1182− let loaded = Loaded::cached(interp, loaded)?;
1183− let mut standing = loaded.line_standing(interp, c)?;
1184− standing.focus |= as_focus;
1185− commit_line(interp, c, pal, &standing)
1186−}
1187−
1188fn display_commit_block(
613fn display_commit_block(
1189 interp: &mut Interp,
614 interp: &mut Interp,
1190 c: &Value,
615 c: &Value,
1191 pal: &Palette,
616 pal: &Palette,
1192 indent: usize,
617 indent: usize,
1193 out: &mut String,
618 out: &mut String,
1194− loaded: &Loaded,
1195) -> Result<(), Crash> {
619) -> Result<(), Crash> {
1196 let pad = " ".repeat(indent);
620 let pad = " ".repeat(indent);
621+ out.push_str(&pad);
622+ out.push_str(&commit_line(interp, c, pal, true, &BTreeSet::new(), false)?);
623+ out.push('\n');
624+ // author · age · n files — from metadata by id (§5.1)
1197 let id = match c.field("id")? {
625 let id = match c.field("id")? {
1198 Value::Id(i) => i.to_string(),
626 Value::Id(i) => i.to_string(),
1199 _ => String::new(),
627 _ => String::new(),
1200 };
628 };
1201− let own = snapshot_map_of(&c.field("files")?)?;
629+ let files = c.field("files").and_then(|v| v.as_list().map(|x| x.to_vec()))?;
1202− // the paths it changes against its parent in the given repository; with
1203− // no parent there — the root, or a commit the expression made, whose
1204− // parent the value does not record — what it changes is not known
1205− let touched = match loaded.parents.get(&id) {
1206− Some(parent) => Some(touched_in_maps(
1207− &snapshot_map_of(&parent.field("files")?)?,
1208− &own,
1209− )?),
1210− None => None,
1211− };
1212− // the line `tree` draws for it (§5.1: "a `tree` line without rails")
1213− let empty = touched.as_ref().is_some_and(|t| t.is_empty());
1214− out.push_str(&pad);
1215− out.push_str(&commit_line(interp, c, pal, &loaded.standing(&id, empty))?);
1216− out.push('\n');
1217− // author · age · n files — from metadata by id (§5.1)
1218− let n = touched.as_ref().map_or(own.len(), |t| t.len());
1219 if let Ok(meta) = interp.backend.meta(&id) {
630 if let Ok(meta) = interp.backend.meta(&id) {
1220 let age = render_age(meta.time);
631 let age = render_age(meta.time);
1221 out.push_str(&pad);
632 out.push_str(&pad);
1222− out.push_str(&format!(" {} · {} · {} files\n", meta.author, age, n));
633+ out.push_str(&format!(
1223− }
634+ " {} · {} · {} files\n",
1224− if n > 0 {
635+ meta.author,
1225− out.push('\n');
636+ age,
637+ files.len()
638+ ));
1226 }
639 }
1227− match &touched {
640+ for e in files.iter() {
1228− Some(touched) => {
641+ let path = path_string(&e.field("path")?)?;
1229− for (path, mark, _) in touched {
642+ let content = e.field("content")?;
1230− out.push_str(&pad);
1231− out.push_str(&format!(
1232− " {} {}\n",
1233− colored_mark(*mark, pal),
1234− path.join("/")
1235− ));
1236− }
1237− }
1238− // its files, unmarked but for `✖`
1239− None => {
1240− for (path, content) in &own {
1241− out.push_str(&pad);
1242− if matches!(content, Value::Blob(b) if b.is_unresolved()) {
643+ let unresolved = matches!(&content, Value::Blob(b) if b.is_unresolved());
644+ out.push_str(&pad);
645+ if unresolved {
1243− out.push_str(&format!(" {} {}\n", pal.red("✖"), path.join("/")));
646+ out.push_str(&format!(" {} {}\n", pal.red("✖"), path));
1244− } else {
647+ } else {
1245− out.push_str(&format!(" {}\n", path.join("/")));
648+ out.push_str(&format!(" {}\n", path));
1246− }
1247− }
1248 }
649 }
1249 }
650 }
1250 Ok(())
651 Ok(())
@@ -1289,25 +690,19 @@ pub fn render_date(time: i64) -> String {
1289 format!("{:04}-{:02}-{:02}", y, m, d)
690 format!("{:04}-{:02}-{:02}", y, m, d)
1290}
691}
1291
692
1292−/// the repo value in context (the one the expression was given), for
1293−/// resolving ids to commits
693+/// the repo value in context (the loaded repo), for resolving ids to commits
1294fn top_of(interp: &Interp, _x: &Value) -> Result<Value, Crash> {
694fn top_of(interp: &Interp, _x: &Value) -> Result<Value, Crash> {
695+ let old = interp.old_repo.borrow().clone();
1295− given_repo(interp).ok_or_else(|| Crash::new("display: no repository in context"))
696+ old.ok_or_else(|| Crash::new("display: no repository in context"))
1296}
697}
1297
698
1298−/// `list` in block form, the blocks it holds left on `todo` (see `Block`)
1299−#[allow(clippy::too_many_arguments)]
1300fn display_list_block(
699fn display_list_block(
1301 interp: &mut Interp,
700 interp: &mut Interp,
1302− list: &ListVal,
701+ xs: &[Value],
1303 pal: &Palette,
702 pal: &Palette,
1304 indent: usize,
703 indent: usize,
1305 out: &mut String,
704 out: &mut String,
1306− loaded: &mut Option<Loaded>,
1307− todo: &mut Vec<Block>,
1308− term_w: Option<usize>,
1309) -> Result<(), Crash> {
705) -> Result<(), Crash> {
1310− let xs: &[Value] = list;
1311 let pad = " ".repeat(indent);
706 let pad = " ".repeat(indent);
1312 if xs.is_empty() {
707 if xs.is_empty() {
1313 out.push_str(&pad);
708 out.push_str(&pad);
@@ -1317,29 +712,22 @@ fn display_list_block(
1317 }
712 }
1318 // 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)
1319 if xs.iter().all(|x| matches!(x, Value::Id(_))) {
714 if xs.iter().all(|x| matches!(x, Value::Id(_))) {
1320− // index the history once: a `by_id` walk per id made printing a
1321− // revset quadratic in the size of the history
1322− let repo = top_of(interp, &xs[0])?;
1323− let mut commits: BTreeMap<String, Value> = BTreeMap::new();
1324− for c in crate::repo::all_commits(&repo)? {
1325− if let Ok(Value::Id(i)) = c.field("id") {
1326− commits.insert(i.to_string(), c);
1327− }
1328− }
1329− let loaded = Loaded::cached(interp, loaded)?;
1330 for x in xs {
715 for x in xs {
1331 let id = match x {
716 let id = match x {
1332 Value::Id(i) => i.to_string(),
717 Value::Id(i) => i.to_string(),
1333 _ => unreachable!(),
718 _ => unreachable!(),
1334 };
719 };
1335 out.push_str(&pad);
720 out.push_str(&pad);
1336− match commits.get(&id) {
721+ let repo = top_of(interp, x)?;
722+ match crate::repo::by_id(&repo, &id)? {
1337− Some(c) => {
723+ Some(loc) => {
1338− let c = c.clone();
724+ let c = loc.field("root")?;
1339− // the glyph of its line form (§5.1)
725+ let conflict = has_conflict(&c)?;
1340− let glyph = node_glyph(&c, &id, &loaded.line_standing(interp, &c)?)?;
726+ let glyph = if conflict {
1341− let conflict = glyph == "⊗";
727+ pal.red("⊗")
1342− let glyph = pal.code(node_ansi(&glyph), &glyph);
728+ } else {
729+ "○".to_string()
730+ };
1343 let msg = c.field("message")?.as_text()?.to_string();
731 let msg = c.field("message")?.as_text()?.to_string();
1344 let msg_first = msg.lines().next().unwrap_or("");
732 let msg_first = msg.lines().next().unwrap_or("");
1345 let labels: Vec<String> = c
733 let labels: Vec<String> = c
@@ -1349,7 +737,7 @@ fn display_list_block(
1349 .map(|l| l.as_text().map(|s| s.to_string()))
737 .map(|l| l.as_text().map(|s| s.to_string()))
1350 .collect::<Result<_, _>>()?;
738 .collect::<Result<_, _>>()?;
1351 let id_s = if conflict {
739 let id_s = if conflict {
1352− pal.red(&interp.unique_prefix(&id))
740+ pal.red(&interp.backend.unique_prefix(&id))
1353 } else {
741 } else {
1354 display_id(interp, &id, pal)
742 display_id(interp, &id, pal)
1355 };
743 };
@@ -1361,7 +749,7 @@ fn display_list_block(
1361 glyph,
749 glyph,
1362 id_s,
750 id_s,
1363 msg_first,
751 msg_first,
1364− pal.label(&labels.join(" "))
752+ pal.green(&labels.join(" "))
1365 ));
753 ));
1366 }
754 }
1367 }
755 }
@@ -1420,22 +808,20 @@ fn display_list_block(
1420 _ => (String::new(), false),
808 _ => (String::new(), false),
1421 };
809 };
1422 out.push_str(&pad);
810 out.push_str(&pad);
1423− let mark = if unresolved { pal.red("✖") } else { " ".into() };
811+ let mark = if unresolved { "✖" } else { " " };
1424 let size_s = if unresolved {
812 let size_s = if unresolved {
1425 pal.red(&size)
813 pal.red(&size)
1426 } else {
814 } else {
1427− pal.yellow(&size)
815+ size
1428 };
816 };
1429 // pad by display width, not by character count: `{:n$}`
817 // pad by display width, not by character count: `{:n$}`
1430 // pads to n *chars*, so a wide (East Asian) path pushes
818 // pads to n *chars*, so a wide (East Asian) path pushes
1431 // the size column out of line
819 // the size column out of line
1432− out.push_str(&format!(
1433− "{} {} {}\n", mark, pal.cyan(&pad_right(&path, width_max)), size_s
820+ out.push_str(&format!("{} {} {}\n", mark, pad_right(&path, width_max), size_s));
1434− ));
1435 }
821 }
1436 return Ok(());
822 return Ok(());
1437 }
823 }
1438− return display_table(interp, xs, pal, indent, out, loaded, term_w);
824+ return display_table(interp, xs, pal, indent, out);
1439 }
825 }
1440 }
826 }
1441 // list of other scalars
827 // list of other scalars
@@ -1448,8 +834,13 @@ fn display_list_block(
1448 out.push('\n');
834 out.push('\n');
1449 return Ok(());
835 return Ok(());
1450 }
836 }
1451− // one block per item, separated by a blank line (see `Block`)
837+ // one block per item, separated by a blank line
1452− todo.push(Block::Items { xs: list.clone(), next: 0, indent });
838+ for (i, x) in xs.iter().enumerate() {
839+ if i > 0 {
840+ out.push('\n');
841+ }
842+ display_block(interp, x, pal, indent, out)?;
843+ }
1453 Ok(())
844 Ok(())
1454}
845}
1455
846
@@ -1459,38 +850,18 @@ fn display_table(
1459 pal: &Palette,
850 pal: &Palette,
1460 indent: usize,
851 indent: usize,
1461 out: &mut String,
852 out: &mut String,
1462− loaded: &mut Option<Loaded>,
1463− term_w: Option<usize>,
1464) -> Result<(), Crash> {
853) -> Result<(), Crash> {
1465 let pad = " ".repeat(indent);
854 let pad = " ".repeat(indent);
1466 let fields: Vec<String> = field_set(&xs[0]).unwrap().into_iter().collect();
855 let fields: Vec<String> = field_set(&xs[0]).unwrap().into_iter().collect();
1467− // IDs must distinguish both stored commits and IDs created by this expression.
1468− let mut ids: Vec<String> = xs.iter().flat_map(|x| fields.iter().filter_map(|f| {
1469− match x.field(f) { Ok(Value::Id(id)) => Some(id.to_string()), _ => None }
1470− })).collect();
1471− ids.sort();
1472− ids.dedup();
1473− let mut fixed = vec![false; fields.len()];
1474 // cells
856 // cells
1475 let mut rows: Vec<Vec<String>> = Vec::new();
857 let mut rows: Vec<Vec<String>> = Vec::new();
1476 for x in xs {
858 for x in xs {
1477 let mut row = Vec::new();
859 let mut row = Vec::new();
1478− for (column, f) in fields.iter().enumerate() {
860+ for f in &fields {
1479 let cell = match x.field(f)? {
861 let cell = match x.field(f)? {
1480− Value::Bool(true) => pal.green("✓"),
862+ Value::Bool(true) => "✓".to_string(),
1481 Value::Bool(false) => String::new(),
863 Value::Bool(false) => String::new(),
1482− Value::Id(id) => {
1483− fixed[column] = true;
1484− let stored = interp.unique_prefix(&id);
1485− let shown = crate::eval::unique_prefix_in(&ids, &id);
1486− let prefix = if stored.len() > shown.len() { stored } else { shown };
1487− pal.id(&format!("@{}", prefix))
1488− }
1489− Value::List(values) if values.iter().all(|v| matches!(v, Value::Text(_))) => {
1490− values.iter().map(|v| v.as_text().map(first_line))
1491− .collect::<Result<Vec<_>, _>>()?.join(", ")
1492− }
1493− other => display_line(interp, &other, pal, loaded)?,
864+ other => display_line(interp, &other, pal)?,
1494 };
865 };
1495 row.push(cell);
866 row.push(cell);
1496 }
867 }
@@ -1500,7 +871,7 @@ fn display_table(
1500 let keep: Vec<usize> = (0..fields.len())
871 let keep: Vec<usize> = (0..fields.len())
1501 .filter(|&i| rows.iter().any(|r| !r[i].is_empty()))
872 .filter(|&i| rows.iter().any(|r| !r[i].is_empty()))
1502 .collect();
873 .collect();
1503− let mut widths: Vec<usize> = keep
874+ let widths: Vec<usize> = keep
1504 .iter()
875 .iter()
1505 .map(|&i| {
876 .map(|&i| {
1506 rows.iter()
877 rows.iter()
@@ -1510,40 +881,6 @@ fn display_table(
1510 .max(width(&fields[i]))
881 .max(width(&fields[i]))
1511 })
882 })
1512 .collect();
883 .collect();
1513− if let Some(terminal) = term_w {
1514− let available = terminal.saturating_sub(indent);
1515− let gaps = keep.len().saturating_sub(1) * 3;
1516− let minimum: Vec<usize> = keep.iter().zip(&widths).map(|(&i, &w)| {
1517− if fixed[i] { w } else { w.min(width(&fields[i]).max(6)) }
1518− }).collect();
1519− if minimum.iter().sum::<usize>() + gaps > available {
1520− // A narrow terminal cannot hold the headers and identifiers side by
1521− // side. Keep a small nesting inset below the parent key, instead of
1522− // letting a long parent field name consume the entire line width.
1523− let inset = indent.min(2).min(terminal / 4);
1524− let pad = " ".repeat(inset);
1525− let available = terminal.saturating_sub(inset);
1526− for (n, row) in rows.iter().enumerate() {
1527− if n > 0 { out.push('\n'); }
1528− for &i in &keep {
1529− let text = format!("{}: {}", pal.field(&fields[i]), row[i]);
1530− for line in wrap_display_text(&text, available.max(1)) {
1531− out.push_str(&pad);
1532− out.push_str(&line);
1533− out.push('\n');
1534− }
1535− }
1536− }
1537− return Ok(());
1538− }
1539− let mut excess = (widths.iter().sum::<usize>() + gaps).saturating_sub(available);
1540− while excess > 0 {
1541− let column = (0..widths.len()).filter(|&i| widths[i] > minimum[i])
1542− .max_by_key(|&i| widths[i]).expect("minimum widths fit");
1543− widths[column] -= 1;
1544− excess -= 1;
1545− }
1546− }
1547 // header
884 // header
1548 out.push_str(&pad);
885 out.push_str(&pad);
1549 let header: Vec<String> = keep
886 let header: Vec<String> = keep
@@ -1551,14 +888,14 @@ fn display_table(
1551 .zip(&widths)
888 .zip(&widths)
1552 .map(|(&i, w)| pad_right(&fields[i], *w))
889 .map(|(&i, w)| pad_right(&fields[i], *w))
1553 .collect();
890 .collect();
1554− out.push_str(&pal.field(&header.join(" ")));
891+ out.push_str(&pal.dim(&header.join(" ")));
1555 out.push('\n');
892 out.push('\n');
1556 for r in &rows {
893 for r in &rows {
1557 out.push_str(&pad);
894 out.push_str(&pad);
1558 let cells: Vec<String> = keep
895 let cells: Vec<String> = keep
1559 .iter()
896 .iter()
1560 .zip(&widths)
897 .zip(&widths)
1561− .map(|(&i, w)| pad_right(&cut_to_width(&r[i], *w), *w))
898+ .map(|(&i, w)| pad_right(&r[i], *w))
1562 .collect();
899 .collect();
1563 out.push_str(&cells.join(" "));
900 out.push_str(&cells.join(" "));
1564 out.push('\n');
901 out.push('\n');
@@ -1599,28 +936,7 @@ fn default_tree_options(_interp: &Interp) -> Result<TreeOptions, Crash> {
1599 })
936 })
1600}
937}
1601
938
1602−/// The terminal's width when stdout is one — what rows are cut to and the
1603−/// legend is placed by (specs/tree.md Step 4, §Legend) — else `None`.
1604−fn tty_width() -> Option<usize> {
1605− if crate::show::stdout_is_tty() {
1606− crate::show::terminal_width()
1607− } else {
1608− None
1609− }
1610−}
1611−
1612pub fn tree_with(interp: &mut Interp, opts: &Value, repo: &Value) -> Result<Value, Crash> {
939pub fn tree_with(interp: &mut Interp, opts: &Value, repo: &Value) -> Result<Value, Crash> {
1613− tree_with_width(interp, opts, repo, tty_width())
1614−}
1615−
1616−/// `treeWith` as if stdout were a terminal `term_w` columns wide (`None`: not
1617−/// a terminal). Public so the tests can drive truncation without a pty.
1618−pub fn tree_with_width(
1619− interp: &mut Interp,
1620− opts: &Value,
1621− repo: &Value,
1622− term_w: Option<usize>,
1623−) -> Result<Value, Crash> {
1624 let defaults = default_tree_options(interp)?;
940 let defaults = default_tree_options(interp)?;
1625 // a missing option falls back to its default, so an older config that
941 // a missing option falls back to its default, so an older config that
1626 // predates a newer option keeps working; a present option is type-checked
942 // predates a newer option keeps working; a present option is type-checked
@@ -1667,7 +983,7 @@ pub fn tree_with_width(
1667 date,
983 date,
1668 files,
984 files,
1669 };
985 };
1670− let text = tree_render(interp, &o, repo, &pal, true, term_w)?;
986+ let text = tree_render(interp, &o, repo, &pal, true)?;
1671 Ok(Value::text(text))
987 Ok(Value::text(text))
1672}
988}
1673
989
@@ -1682,8 +998,8 @@ struct CommitInfo {
1682 is_ancestor_of_focus: bool,
998 is_ancestor_of_focus: bool,
1683 is_child_of_ancestor: bool,
999 is_child_of_ancestor: bool,
1684 meta: Option<crate::domain::MetaInfo>,
1000 meta: Option<crate::domain::MetaInfo>,
1685− size: Option<usize>, // lines added+removed against parent, up to SIZE_BAR_TOP
1001+ size: Option<usize>, // lines added+removed against parent
1686− nfiles: usize, // number of files changed against parent (`files` column)
1002+ nfiles: usize, // number of files in this commit's snapshot
1687 detail_marks: Vec<(String, char)>,
1003 detail_marks: Vec<(String, char)>,
1688 children: Vec<CommitInfo>,
1004 children: Vec<CommitInfo>,
1689}
1005}
@@ -1709,49 +1025,24 @@ impl CommitInfo {
1709 }
1025 }
1710}
1026}
1711
1027
1712−/// A parent commit's snapshot as seen by its children: the files value,
1028+/// A parent commit's snapshot as seen by its children: the files value, plus
1713−/// loaded on first use — by the parent itself or by whichever child needs to
1714−/// compare against it, since only the child knows that it does — plus its
1715−/// path-keyed map built at most once and shared by every child's diff.
1029+/// its path-keyed map built at most once and shared by every child's diff.
1716/// Keying a snapshot allocates a `Vec<String>` per entry, so rebuilding it for
1030/// Keying a snapshot allocates a `Vec<String>` per entry, so rebuilding it for
1717/// each child dominated `treeFull`/`treeData` on a large history.
1031/// each child dominated `treeFull`/`treeData` on a large history.
1718struct ParentSnap<'a> {
1032struct ParentSnap<'a> {
1719− commit: &'a Value,
1720− files: &'a std::cell::OnceCell<Value>,
1033+ files: &'a Value,
1721 map: &'a std::cell::OnceCell<SnapMap>,
1034 map: &'a std::cell::OnceCell<SnapMap>,
1722− /// the commit's id when its files are still the stored tree
1723− stored: Option<&'a str>,
1724}
1035}
1725
1036
1726impl ParentSnap<'_> {
1037impl ParentSnap<'_> {
1727− fn files(&self) -> Result<&Value, Crash> {
1728− if self.files.get().is_none() {
1729− let _ = self.files.set(self.commit.field("files")?);
1730− }
1731− Ok(self.files.get().expect("just set"))
1732− }
1733−
1734 fn map(&self) -> Result<&SnapMap, Crash> {
1038 fn map(&self) -> Result<&SnapMap, Crash> {
1735 if self.map.get().is_none() {
1039 if self.map.get().is_none() {
1736− let _ = self.map.set(snapshot_map_of(self.files()?)?);
1040+ let _ = self.map.set(snapshot_map_of(self.files)?);
1737 }
1041 }
1738 Ok(self.map.get().expect("just set"))
1042 Ok(self.map.get().expect("just set"))
1739 }
1043 }
1740}
1044}
1741
1045
1742−/// The id of the stored commit whose tree `commit`'s files are, read without
1743−/// loading them: the tag on the backend's lazy list (`ThunkVal::origin`). An
1744−/// edit that changes the files replaces that list, so this is `None` for them.
1745−fn stored_origin(commit: &Value) -> Option<&str> {
1746− match commit {
1747− Value::Record(m) => match m.get("files") {
1748− Some(Value::Thunk(t)) => t.origin(),
1749− _ => None,
1750− },
1751− _ => None,
1752− }
1753−}
1754−
1755#[allow(clippy::too_many_arguments)]
1046#[allow(clippy::too_many_arguments)]
1756fn build_info(
1047fn build_info(
1757 interp: &mut Interp,
1048 interp: &mut Interp,
@@ -1778,13 +1069,9 @@ fn build_info(
1778 .map(|l| l.as_text().map(|s| s.to_string()))
1069 .map(|l| l.as_text().map(|s| s.to_string()))
1779 .collect::<Result<_, _>>()?;
1070 .collect::<Result<_, _>>()?;
1780 // conflict and empty are O(1) backend queries when the backend can answer
1071 // conflict and empty are O(1) backend queries when the backend can answer
1781− // them from tree ids (jj), without touching the file list at all. Those
1072+ // them from tree ids (jj), without touching the file list at all; the
1782− // answers are about the stored commits, so they are asked only while this
1783− // commit — and, for empty, its parent here — still holds its stored files:
1784− // a dry run (§1.2) renders edits that keep their ids but not their files.
1785− // Otherwise, as on the in-memory backend, fall back to walking the files.
1073+ // in-memory backend cannot, so fall back to walking the files
1786− let stored = stored_origin(commit).filter(|o| *o == id);
1787− let conflict = match stored.and_then(|s| interp.backend.has_conflict(s)) {
1074+ let conflict = match interp.backend.has_conflict(&id) {
1788 Some(c) => c,
1075 Some(c) => c,
1789 None => has_conflict(commit)?,
1076 None => has_conflict(commit)?,
1790 };
1077 };
@@ -1797,67 +1084,67 @@ fn build_info(
1797 matches!(c.field("root").and_then(|r| r.field("id")), Ok(Value::Id(i)) if *i == focus_id)
1084 matches!(c.field("root").and_then(|r| r.field("id")), Ok(Value::Id(i)) if *i == focus_id)
1798 });
1085 });
1799 let want_diff = opts.detail >= 2 || is_focus_pre || child_is_focus || parent_is_focus;
1086 let want_diff = opts.detail >= 2 || is_focus_pre || child_is_focus || parent_is_focus;
1800− let backend_empty = match (stored, parent_files.and_then(|p| p.stored)) {
1801− (Some(me), Some(parent)) => interp.backend.is_empty(me, parent),
1087+ let backend_empty = interp.backend.is_empty(&id);
1088+ // the files list is only materialized when something needs it: the diff
1089+ // (detail/focus), the files data column, or the emptiness fallback
1090+ let need_files = want_diff || opts.files || backend_empty.is_none();
1091+ let files = if need_files {
1092+ Some(commit.field("files")?)
1093+ } else {
1802− _ => None,
1094+ None
1803 };
1095 };
1096+ let nfiles = files.as_ref().map(|f| f.as_list().map(|l| l.len())).transpose()?.unwrap_or(0);
1804− // this commit's own snapshot and its map, each built on first use — here
1097+ // this commit's own snapshot map, built on first use and shared with every
1805− // or by a child that compares against it — and shared with every child
1098+ // child that diffs against it
1806− let my_files: std::cell::OnceCell<Value> = std::cell::OnceCell::new();
1807 let my_map: std::cell::OnceCell<SnapMap> = std::cell::OnceCell::new();
1099 let my_map: std::cell::OnceCell<SnapMap> = std::cell::OnceCell::new();
1808− let my_snap = ParentSnap {
1100+ let my_snap = files.as_ref().map(|f| ParentSnap {
1809− commit,
1810− files: &my_files,
1101+ files: f,
1811 map: &my_map,
1102 map: &my_map,
1812− stored,
1813− };
1103+ });
1814− // the files list is only materialized when something needs it: the diff
1815− // (detail/focus), the files data column, or the emptiness fallback
1816− let (empty, size, detail_marks, nfiles) = match parent_files {
1104+ let (empty, size, detail_marks) = match parent_files {
1817 Some(parent) => {
1105 Some(parent) => {
1106+ let pf = parent.files;
1818 let empty = match backend_empty {
1107 let empty = match backend_empty {
1819 Some(e) => e,
1108 Some(e) => e,
1820− // in any order: a snapshot stands for a tree (§7.3)
1109+ None => crate::value::value_eq(files.as_ref().expect("files loaded"), pf)?,
1821− None => crate::repo::snapshot_eq(my_snap.files()?, parent.files()?)?,
1822 };
1110 };
1823 if want_diff {
1111 if want_diff {
1824 // both maps are built at most once per commit and reused by
1112 // both maps are built at most once per commit and reused by
1825 // this commit's children, so a diff costs one keying, not four
1113 // this commit's children, so a diff costs one keying, not four
1826 let from_map = parent.map()?;
1114 let from_map = parent.map()?;
1827− let to_map = my_snap.map()?;
1115+ let to_map = my_snap.as_ref().expect("files loaded").map()?;
1828 let marks = touched_in_maps(from_map, to_map)?;
1116 let marks = touched_in_maps(from_map, to_map)?;
1829− // size: lines added plus removed against the parent (§7.11
1117+ // size: lines added+removed against the parent
1830− // column 5); a path changed without a changed line (an empty
1831− // file, a new file type) counts one, so only an empty commit
1832− // has no bar. The count stops where the bar does.
1833 let mut lines = 0usize;
1118 let mut lines = 0usize;
1834 for (key, _, _) in &marks {
1119 for (key, _, _) in &marks {
1835− if lines >= SIZE_BAR_TOP {
1120+ let count = |m: &SnapMap| -> usize {
1836− break;
1121+ match m.get(key) {
1837− }
1122+ Some(Value::Blob(b)) => b.line_count().max(1),
1838− let cap = SIZE_BAR_TOP - lines;
1123+ _ => 1,
1124+ }
1125+ };
1839− lines += changed_lines(from_map.get(key), to_map.get(key), cap)?.max(1);
1126+ lines += count(from_map) + count(to_map);
1840 }
1127 }
1841 let marks2: Vec<(String, char)> =
1128 let marks2: Vec<(String, char)> =
1842 marks.iter().map(|(p, m, _)| (p.join("/"), *m)).collect();
1129 marks.iter().map(|(p, m, _)| (p.join("/"), *m)).collect();
1843− (marks.is_empty(), Some(lines), marks2, marks.len())
1130+ (marks.is_empty(), Some(lines), marks2)
1844 } else {
1131 } else {
1845− // the files column is the number of paths changed against the
1846− // parent (specs/tree.md Step 4), so it diffs even without a bar
1847− let nfiles = if opts.files && !empty {
1848− touched_in_maps(parent.map()?, my_snap.map()?)?.len()
1849− } else {
1850− 0
1851− };
1852− (empty, None, Vec::new(), nfiles)
1132+ (empty, None, Vec::new())
1853 }
1133 }
1854 }
1134 }
1855 None => {
1135 None => {
1856− // the top of the history: no parent to compare against, and so no
1136+ // No parent snapshot to compare against — either this is the top
1857− // backend answer either — it is empty iff it has no files (the
1137+ // of the history, or the parent's file list was never materialized
1858− // root never counts as empty), and every file it has is added
1138+ // because nothing needed it. `nfiles` is 0 in that second case
1859− let nfiles = my_snap.files()?.as_list()?.len();
1139+ // simply because the list was not loaded, so it cannot stand in
1140+ // for emptiness: ask the backend first, and fall back to the file
1141+ // count only when the list really was loaded (which is exactly
1142+ // when the backend could not answer).
1143+ let empty = match backend_empty {
1144+ Some(e) => e && id != ROOT_ID,
1145+ None => nfiles == 0 && id != ROOT_ID,
1146+ };
1860− (nfiles == 0 && id != ROOT_ID, None, Vec::new(), nfiles)
1147+ (empty, None, Vec::new())
1861 }
1148 }
1862 };
1149 };
1863 let meta = interp.backend.meta(&id).ok();
1150 let meta = interp.backend.meta(&id).ok();
@@ -1873,7 +1160,7 @@ fn build_info(
1873 immutable,
1160 immutable,
1874 focus_id,
1161 focus_id,
1875 anc,
1162 anc,
1876− Some(&my_snap),
1163+ my_snap.as_ref(),
1877 with_focus,
1164 with_focus,
1878 is_ancestor_of_focus,
1165 is_ancestor_of_focus,
1879 is_focus_pre,
1166 is_focus_pre,
@@ -1903,7 +1190,6 @@ fn tree_render(
1903 repo: &Value,
1190 repo: &Value,
1904 pal: &Palette,
1191 pal: &Palette,
1905 with_focus: bool,
1192 with_focus: bool,
1906− term_w: Option<usize>,
1907) -> Result<String, Crash> {
1193) -> Result<String, Crash> {
1908 // immutable set
1194 // immutable set
1909 let immutable = crate::repo::compute_immutable(interp, repo).unwrap_or_default();
1195 let immutable = crate::repo::compute_immutable(interp, repo).unwrap_or_default();
@@ -1957,37 +1243,39 @@ fn tree_render(
1957 // Step 2 — row order (topological, oldest first)
1243 // Step 2 — row order (topological, oldest first)
1958 let mut flat: Vec<&Display> = Vec::new();
1244 let mut flat: Vec<&Display> = Vec::new();
1959 flatten_display(&droot, &mut flat);
1245 flatten_display(&droot, &mut flat);
1960− // Display uids are dense. Keep the preorder position as the heap's
1246+ let preorder: BTreeMap<usize, usize> =
1961− // second key, so a newly ready descendant wins a time tie with a later
1247+ flat.iter().enumerate().map(|(i, n)| (n.uid(), i)).collect();
1962− // sibling just as it does in the specification's ready set.
1248+ let mut ready: Vec<&Display> = vec![&droot];
1963− let mut preorder = vec![0; flat.len()];
1964− for (i, n) in flat.iter().enumerate() {
1965− preorder[n.uid()] = i;
1966− }
1967− let mut ready = BinaryHeap::from([Reverse((droot.time(), 0usize))]);
1968 let mut rows: Vec<&Display> = Vec::new();
1249 let mut rows: Vec<&Display> = Vec::new();
1969− while let Some(Reverse((_, i))) = ready.pop() {
1250+ while !ready.is_empty() {
1970− let n = flat[i];
1251+ let best = ready
1252+ .iter()
1253+ .enumerate()
1254+ .min_by_key(|(_, n)| (n.time(), preorder[&n.uid()]))
1255+ .map(|(i, _)| i)
1256+ .unwrap();
1257+ let n = ready.remove(best);
1971 rows.push(n);
1258 rows.push(n);
1972 for c in n.children() {
1259 for c in n.children() {
1973− ready.push(Reverse((c.time(), preorder[c.uid()])));
1260+ ready.push(c);
1974 }
1261 }
1975 }
1262 }
1976 // Step 3 — lanes
1263 // Step 3 — lanes
1977− let index = RowIndex::new(&rows);
1978− let placements = assign_lanes(&rows, &index, &trunk, lanes_n);
1264+ let placements = assign_lanes(&rows, &trunk, lanes_n);
1979 // id column: shortest unique prefix among the display tree's commits, min 4
1265 // id column: shortest unique prefix among the display tree's commits, min 4
1980− // (sorted once, so each row costs a lookup rather than a scan of them all)
1981− let mut ids: Vec<String> = rows
1266+ let ids: Vec<&str> = rows
1982 .iter()
1267 .iter()
1983− .filter_map(|n| n.commit().map(|c| c.id.clone()))
1268+ .filter_map(|n| n.commit().map(|c| c.id.as_str()))
1984 .collect();
1269 .collect();
1985− ids.sort();
1986 let prefix_len = |id: &str| -> usize {
1270 let prefix_len = |id: &str| -> usize {
1987− crate::eval::unique_prefix_in(&ids, id).len()
1271+ let mut n = 4.min(id.len());
1272+ while n < id.len() && ids.iter().any(|o| *o != id && o.starts_with(&id[..n])) {
1273+ n += 1;
1274+ }
1275+ n
1988 };
1276 };
1989 // Step 4 — draw
1277 // Step 4 — draw
1990− draw_rows(&rows, &index, &placements, opts, pal, lanes_n, with_focus, &prefix_len, term_w)
1278+ draw_rows(&rows, &placements, opts, pal, lanes_n, with_focus, &prefix_len)
1991}
1279}
1992
1280
1993fn id_of_frame_parent(frame: &Value) -> Result<String, Crash> {
1281fn id_of_frame_parent(frame: &Value) -> Result<String, Crash> {
@@ -2078,16 +1366,12 @@ impl Display {
2078 }
1366 }
2079 }
1367 }
2080
1368
2081− /// The node's time for the row order (Step 1). A node with no stored
2082− /// counterpart (minted in this program) has time ∞: it sorts after every
2083− /// stored one, not first as a `None` would.
2084− fn time(&self) -> i64 {
1369+ fn time(&self) -> Option<i64> {
2085− let t = match self {
1370+ match self {
2086 Display::Commit { info, .. } => info.meta.as_ref().map(|m| m.time),
1371 Display::Commit { info, .. } => info.meta.as_ref().map(|m| m.time),
2087 Display::Run { time, .. } => *time,
1372 Display::Run { time, .. } => *time,
2088 Display::Collapsed { info, .. } => info.meta.as_ref().map(|m| m.time),
1373 Display::Collapsed { info, .. } => info.meta.as_ref().map(|m| m.time),
2089− };
1374+ }
2090− t.unwrap_or(i64::MAX)
2091 }
1375 }
2092
1376
2093 fn children(&self) -> &[Display] {
1377 fn children(&self) -> &[Display] {
@@ -2248,10 +1532,6 @@ enum Rail {
2248struct Placement {
1532struct Placement {
2249 lane: Option<usize>,
1533 lane: Option<usize>,
2250 fork: Option<(usize, bool)>, // (source lane, source empties below)
1534 fork: Option<(usize, bool)>, // (source lane, source empties below)
2251− // on a flattened last child of the trunk head (or of the root of an empty
2252− // trunk): its parent's lane, which empties below this row though no fork
2253− // is drawn (Step 3, §Overflow)
2254− ends: Option<usize>,
2255 reservations: Vec<(usize, usize)>, // (lane, child uid)
1535 reservations: Vec<(usize, usize)>, // (lane, child uid)
2256 flattened: bool,
1536 flattened: bool,
2257}
1537}
@@ -2263,70 +1543,26 @@ fn mark_subtree(n: &Display, set: &mut BTreeSet<usize>) {
2263 }
1543 }
2264}
1544}
2265
1545
2266−/// Relations in display-row order, indexed once for lane assignment and
2267−/// size bars. Finding a parent or the focus afresh on every row made even a
2268−/// single line of history take quadratic time.
2269−struct RowIndex {
2270− row_of: Vec<usize>,
2271− parent: Vec<Option<usize>>,
2272− last_child: Vec<Option<usize>>,
2273− focus_adjacent: Vec<bool>,
2274−}
2275−
2276−impl RowIndex {
2277− fn new(rows: &[&Display]) -> Self {
2278− let mut index = RowIndex {
2279− row_of: vec![0; rows.len()],
2280− parent: vec![None; rows.len()],
2281− last_child: vec![None; rows.len()],
2282− focus_adjacent: vec![false; rows.len()],
2283− };
2284− for (i, n) in rows.iter().enumerate() {
2285− index.row_of[n.uid()] = i;
2286− }
2287− for (i, n) in rows.iter().enumerate() {
2288− for c in n.children() {
2289− let child = index.row_of[c.uid()];
2290− index.parent[child] = Some(i);
2291− index.last_child[i] = Some(index.last_child[i].map_or(child, |last| last.max(child)));
2292− }
2293− }
2294− if let Some(focus) = rows.iter().position(|n| n.commit().is_some_and(|c| c.is_focus)) {
2295− if let Some(parent) = index.parent[focus] {
2296− index.focus_adjacent[parent] = true;
2297− }
2298− for c in rows[focus].children() {
2299− index.focus_adjacent[index.row_of[c.uid()]] = true;
2300− }
2301− }
2302− index
2303− }
2304−}
2305−
2306−fn assign_lanes(
2307− rows: &[&Display],
2308− index: &RowIndex,
2309− trunk: &BTreeSet<String>,
1546+fn assign_lanes(rows: &[&Display], trunk: &BTreeSet<String>, lanes_n: usize) -> Vec<Placement> {
2310− lanes_n: usize,
2311−) -> Vec<Placement> {
2312 let mut rails: Vec<Option<Rail>> = vec![None; lanes_n];
1547 let mut rails: Vec<Option<Rail>> = vec![None; lanes_n];
2313 let mut out: Vec<Placement> = Vec::new();
1548 let mut out: Vec<Placement> = Vec::new();
2314 let mut flattened: BTreeSet<usize> = BTreeSet::new();
1549 let mut flattened: BTreeSet<usize> = BTreeSet::new();
2315− // each node's row: "last child" and "after t" are in row order, which is
2316− // not sibling order once an edit appends a child (`new`, `rebase`)
2317− let row_of = &index.row_of;
2318 for (idx, n) in rows.iter().enumerate() {
1550 for (idx, n) in rows.iter().enumerate() {
2319 let in_trunk = n.in_trunk(trunk);
1551 let in_trunk = n.in_trunk(trunk);
2320− let parent_idx = index.parent[idx];
1552+ let parent = rows[..idx]
2321− let parent = parent_idx.map(|i| rows[i]);
1553+ .iter()
1554+ .copied()
1555+ .find(|p| p.children().iter().any(|c| c.uid() == n.uid()));
2322− let parent_lane = parent_idx.and_then(|i| out[i].lane);
1556+ let parent_lane = parent.and_then(|p| out[rows[..idx].iter().position(|x| x.uid() == p.uid()).unwrap()].lane);
2323− let is_last_child = parent_idx
1557+ let is_last_child = parent
2324− .map(|i| index.last_child[i] == Some(idx))
1558+ .map(|p| {
1559+ let cs = p.children();
1560+ cs[cs.len() - 1].uid() == n.uid()
1561+ })
2325 .unwrap_or(false);
1562 .unwrap_or(false);
2326 let was_flat = flattened.contains(&n.uid());
1563 let was_flat = flattened.contains(&n.uid());
2327 let mut lane: Option<usize> = None;
1564 let mut lane: Option<usize> = None;
2328 let mut fork: Option<(usize, bool)> = None;
1565 let mut fork: Option<(usize, bool)> = None;
2329− let mut ends: Option<usize> = None;
2330 if !was_flat {
1566 if !was_flat {
2331 if idx == 0 {
1567 if idx == 0 {
2332 lane = Some(0);
1568 lane = Some(0);
@@ -2352,18 +1588,7 @@ fn assign_lanes(
2352 fork = Some((pl, false));
1588 fork = Some((pl, false));
2353 }
1589 }
2354 }
1590 }
2355− None => {
2356− // overflow is rule 5 too: when n is p's last
2357− // child (p is then the trunk head or the root of
2358− // an empty trunk, as any other trunk commit
2359− // reserves lanes for its later side children),
2360− // p's lane empties below this row (Step 3)
2361− mark_subtree(n, &mut flattened);
1591+ None => mark_subtree(n, &mut flattened),
2362− if is_last_child {
2363− rails[pl] = None;
2364− ends = Some(pl);
2365− }
2366− }
2367 }
1592 }
2368 }
1593 }
2369 } else {
1594 } else {
@@ -2385,13 +1610,8 @@ fn assign_lanes(
2385 let mut reservations: Vec<(usize, usize)> = Vec::new();
1610 let mut reservations: Vec<(usize, usize)> = Vec::new();
2386 if !now_flat && in_trunk {
1611 if !now_flat && in_trunk {
2387 let cs = n.children();
1612 let cs = n.children();
2388− if let Some(t) = cs.iter().find(|c| c.in_trunk(trunk)) {
1613+ if let Some(tpos) = cs.iter().position(|c| c.in_trunk(trunk)) {
2389− let mut after: Vec<&Display> = cs
2390− .iter()
2391− .filter(|c| row_of[c.uid()] > row_of[t.uid()])
2392− .collect();
2393− after.sort_by_key(|c| row_of[c.uid()]);
2394− for c in after {
1614+ for c in &cs[tpos + 1..] {
2395 if flattened.contains(&c.uid()) {
1615 if flattened.contains(&c.uid()) {
2396 continue;
1616 continue;
2397 }
1617 }
@@ -2408,7 +1628,6 @@ fn assign_lanes(
2408 out.push(Placement {
1628 out.push(Placement {
2409 lane: if now_flat { None } else { lane },
1629 lane: if now_flat { None } else { lane },
2410 fork: if now_flat { None } else { fork },
1630 fork: if now_flat { None } else { fork },
2411− ends,
2412 reservations,
1631 reservations,
2413 flattened: now_flat,
1632 flattened: now_flat,
2414 });
1633 });
@@ -2436,20 +1655,27 @@ struct RowText {
2436
1655
2437fn build_row_text(
1656fn build_row_text(
2438 n: &Display,
1657 n: &Display,
2439− focus_adjacent: bool,
1658+ rows: &[&Display],
2440 opts: &TreeOptions,
1659 opts: &TreeOptions,
2441 pal: &Palette,
1660 pal: &Palette,
2442 with_focus: bool,
1661 with_focus: bool,
2443 prefix_len: &dyn Fn(&str) -> usize,
1662 prefix_len: &dyn Fn(&str) -> usize,
1663+ run_extra: usize,
2444 id_w: usize,
1664 id_w: usize,
2445) -> RowText {
1665) -> RowText {
2446 let c = n.commit();
1666 let c = n.commit();
2447 let is_focus = c.map(|x| x.is_focus).unwrap_or(false);
1667 let is_focus = c.map(|x| x.is_focus).unwrap_or(false);
2448− let gutter = if is_focus && with_focus { pal.code("1;96", "▶ ") } else { " ".into() };
1668+ let gutter = if is_focus && with_focus { "▶ " } else { " " }.to_string();
2449− // id (blank on a run row, whose count `rail_row` draws: the part that
1669+ // id (on a run row the count follows ╎; only the part that overflows the
2450− // overflows the rails area runs on into the id column)
1670+ // rails area spills into the id column)
2451 let id = match n {
1671 let id = match n {
2452− Display::Run { .. } => " ".repeat(id_w),
1672+ Display::Run { .. } => {
1673+ let mut s = String::new();
1674+ while width(&s) < run_extra + id_w {
1675+ s.push(' ');
1676+ }
1677+ s
1678+ }
2453 _ => match c {
1679 _ => match c {
2454 Some(info) => {
1680 Some(info) => {
2455 let k = prefix_len(&info.id);
1681 let k = prefix_len(&info.id);
@@ -2459,10 +1685,8 @@ fn build_row_text(
2459 pal.red(&format!("@{}", p))
1685 pal.red(&format!("@{}", p))
2460 } else if info.immutable {
1686 } else if info.immutable {
2461 pal.blue(&format!("@{}", p))
1687 pal.blue(&format!("@{}", p))
2462− } else if info.is_focus {
2463− pal.code("1;96", &format!("@{}", p))
2464 } else {
1688 } else {
2465− pal.id(&format!("@{}", p))
1689+ format!("@{}", p)
2466 };
1690 };
2467 let plain = k + 1; // +1 for the @
1691 let plain = k + 1; // +1 for the @
2468 let padded = if plain < id_w {
1692 let padded = if plain < id_w {
@@ -2476,8 +1700,23 @@ fn build_row_text(
2476 },
1700 },
2477 };
1701 };
2478 // size bar
1702 // size bar
1703+ let focus_idx = rows
1704+ .iter()
1705+ .position(|m| m.commit().map(|x| x.is_focus).unwrap_or(false));
1706+ let is_focus_parent = focus_idx
1707+ .map(|fi| {
1708+ rows[..fi].iter().any(|p| {
1709+ p.children()
1710+ .iter()
1711+ .any(|ch| ch.uid() == rows[fi].uid()) && p.uid() == n.uid()
1712+ })
1713+ })
1714+ .unwrap_or(false);
1715+ let is_focus_child = focus_idx
1716+ .map(|fi| rows[fi].children().iter().any(|ch| ch.uid() == n.uid()))
1717+ .unwrap_or(false);
2479 let show_bar = opts.detail >= 1
1718 let show_bar = opts.detail >= 1
2480− && c.map(|x| opts.detail >= 2 || x.is_focus || focus_adjacent)
1719+ && c.map(|x| opts.detail >= 2 || x.is_focus || is_focus_parent || is_focus_child)
2481 .unwrap_or(false);
1720 .unwrap_or(false);
2482 let bar = if show_bar {
1721 let bar = if show_bar {
2483 c.map(|x| size_bar(x.size, pal)).unwrap_or_default()
1722 c.map(|x| size_bar(x.size, pal)).unwrap_or_default()
@@ -2509,20 +1748,22 @@ fn build_row_text(
2509 }
1748 }
2510 _ => (String::new(), String::new(), String::new()),
1749 _ => (String::new(), String::new(), String::new()),
2511 };
1750 };
2512− // extra columns (§Step 4): date, files changed, full author name — one
1751+ // extra columns (§Step 4): date, files changed, full author name
2513− // entry per enabled column, blank without metadata (a minted commit), so
2514− // entry k is the same column on every row
2515 let mut meta_extra: Vec<String> = Vec::new();
1752 let mut meta_extra: Vec<String> = Vec::new();
2516 if let Some(info) = c {
1753 if let Some(info) = c {
2517 if info.id != ROOT_ID {
1754 if info.id != ROOT_ID {
2518 if opts.date {
1755 if opts.date {
2519− meta_extra.push(info.meta.as_ref().map(|m| render_date(m.time)).unwrap_or_default());
1756+ if let Some(m) = info.meta.as_ref() {
1757+ meta_extra.push(render_date(m.time));
1758+ }
2520 }
1759 }
2521 if opts.files {
1760 if opts.files {
2522 meta_extra.push(format!("{} files", info.nfiles));
1761 meta_extra.push(format!("{} files", info.nfiles));
2523 }
1762 }
2524 if opts.author {
1763 if opts.author {
2525− meta_extra.push(info.meta.as_ref().map(|m| m.author.clone()).unwrap_or_default());
1764+ if let Some(m) = info.meta.as_ref() {
1765+ meta_extra.push(m.author.clone());
1766+ }
2526 }
1767 }
2527 }
1768 }
2528 }
1769 }
@@ -2542,59 +1783,78 @@ fn build_row_text(
2542#[allow(clippy::too_many_arguments)]
1783#[allow(clippy::too_many_arguments)]
2543fn draw_rows(
1784fn draw_rows(
2544 rows: &[&Display],
1785 rows: &[&Display],
2545− index: &RowIndex,
2546 placements: &[Placement],
1786 placements: &[Placement],
2547 opts: &TreeOptions,
1787 opts: &TreeOptions,
2548 pal: &Palette,
1788 pal: &Palette,
2549 lanes_n: usize,
1789 lanes_n: usize,
2550 with_focus: bool,
1790 with_focus: bool,
2551 prefix_len: &dyn Fn(&str) -> usize,
1791 prefix_len: &dyn Fn(&str) -> usize,
2552− term_w: Option<usize>,
2553) -> Result<String, Crash> {
1792) -> Result<String, Crash> {
2554 let rail_chars = 2 * lanes_n;
1793 let rail_chars = 2 * lanes_n;
2555− let overflow = placements.iter().any(|p| p.flattened);
2556− // each row's rails, drawn once; a run row's go on past the rails area
2557− // when its count does not fit
2558− let mut rail_state = vec![None; lanes_n];
2559− let rails: Vec<(Vec<char>, Vec<bool>)> = rows
2560− .iter()
2561− .zip(placements)
2562− .map(|(n, p)| {
2563− let row = rail_row(n, p, &rail_state, lanes_n, opts);
2564− advance_rail_state(&mut rail_state, n, p);
2565− row
2566− })
2567− .collect();
2568− // id column width: 4-char min prefix plus the `@` literal prefix, and
1794+ // id column width: 4-char min prefix plus the `@` literal prefix, and any
2569− // what a run count writes into it past the rails area and the space
1795+ // overflow of a run count past the rails area
2570− // after it
2571 let mut id_w = 5usize;
1796 let mut id_w = 5usize;
2572− for (chars, _) in &rails {
1797+ for (idx, n) in rows.iter().enumerate() {
2573− id_w = id_w.max(chars.len().saturating_sub(rail_chars + 1));
1798+ if let Display::Run { count, .. } = n {
1799+ if let Some(l) = placements[idx].lane {
1800+ let digits = format!("{}", count).len();
1801+ let extra = (2 * l + 1 + digits).saturating_sub(rail_chars);
1802+ // digits written from 2l+1; rails hold rail_chars; overflow
1803+ // goes into the id column
1804+ if extra > 0 {
1805+ id_w = id_w.max(4 + extra);
1806+ }
1807+ }
1808+ }
2574 }
1809 }
2575 for n in rows {
1810 for n in rows {
2576 if let Some(c) = n.commit() {
1811 if let Some(c) = n.commit() {
2577 id_w = id_w.max(prefix_len(&c.id) + 1); // +1 for the `@`
1812 id_w = id_w.max(prefix_len(&c.id) + 1); // +1 for the `@`
2578 }
1813 }
2579 }
1814 }
1815+ let text_off = 2 + rail_chars + 1;
1816+ let msg_off = text_off + id_w + 2;
2580 let label_col = rows
1817 let label_col = rows
2581 .iter()
1818 .iter()
2582 .any(|n| n.commit().map(|c| !c.labels.is_empty()).unwrap_or(false));
1819 .any(|n| n.commit().map(|c| !c.labels.is_empty()).unwrap_or(false));
2583 // build row texts
1820 // build row texts
2584 let mut texts: Vec<RowText> = Vec::new();
1821 let mut texts: Vec<RowText> = Vec::new();
2585− for (i, n) in rows.iter().enumerate() {
1822+ for (idx, n) in rows.iter().enumerate() {
1823+ let run_extra = match (n, placements[idx].lane) {
1824+ (Display::Run { count, .. }, Some(l)) => {
1825+ let digits = format!("{}", count).len();
1826+ (2 * l + 1 + digits).saturating_sub(rail_chars)
1827+ }
1828+ _ => 0,
1829+ };
1830+ texts.push(build_row_text(
2586− texts.push(build_row_text(n, index.focus_adjacent[i], opts, pal, with_focus, prefix_len, id_w));
1831+ n, rows, opts, pal, with_focus, prefix_len, run_extra, id_w,
1832+ ));
2587− }
1833+ }
2588− // Columns 5–9 start at the same offset on every row (§Step 4). The size
1834+ // right edges for the label and margin columns
2589− // bar's column is there when any row draws a bar — ` ▅`, two columns —
1835+ let mut msg_end_max = 0usize;
2590− // and blank on the rows that draw none, so a bar never shifts a message.
1836+ let mut lab_end_max = 0usize;
2591− let bar_col = texts.iter().any(|t| !t.bar.is_empty());
1837+ for (idx, n) in rows.iter().enumerate() {
2592− // each row up to its message: gutter, rails, id and bar
1838+ let t = &texts[idx];
1839+ if n.commit().is_none() {
1840+ continue;
1841+ }
1842+ let mut pos = msg_off;
1843+ if !t.bar.is_empty() {
1844+ pos += 1; // the bar takes one extra column before the message
1845+ }
1846+ pos += width(&t.msg);
1847+ if !t.labels.is_empty() {
1848+ pos += 2 + width(&t.labels);
1849+ }
1850+ msg_end_max = msg_end_max.max(pos - if t.labels.is_empty() { 0 } else { 2 + width(&t.labels) });
1851+ lab_end_max = lab_end_max.max(pos);
1852+ }
2593− let mut heads: Vec<String> = Vec::new();
1853+ let mut lines: Vec<String> = Vec::new();
2594 for (idx, n) in rows.iter().enumerate() {
1854 for (idx, n) in rows.iter().enumerate() {
2595 let pl = &placements[idx];
1855 let pl = &placements[idx];
2596 let t = &texts[idx];
1856 let t = &texts[idx];
2597− let (chars, lane0) = &rails[idx];
1857+ let (chars, lane0) = rail_row(rows, placements, idx, lanes_n, opts);
2598 let mut line = String::new();
1858 let mut line = String::new();
2599 line.push_str(&t.gutter);
1859 line.push_str(&t.gutter);
2600 // the node glyph (on a commit row) is meaning-coloured; rails lane-coloured
1860 // the node glyph (on a commit row) is meaning-coloured; rails lane-coloured
@@ -2602,119 +1862,59 @@ fn draw_rows(
2602 (Display::Commit { info, .. }, Some(l)) => Some((2 * l, glyph_ansi(info))),
1862 (Display::Commit { info, .. }, Some(l)) => Some((2 * l, glyph_ansi(info))),
2603 _ => None,
1863 _ => None,
2604 };
1864 };
2605− line.push_str(&color_rails(chars, lane0, pal, glyph));
1865+ line.push_str(&color_rails(&chars, &lane0, pal, glyph));
2606− if overflow {
2607− let status = if pl.flattened {
2608− let glyph = n.commit().map(|c| {
2609− pal.code(glyph_ansi(c), glyph_for(c, opts.icons))
2610− }).unwrap_or_default();
2611− format!("» {}", glyph)
2612− } else {
2613− String::new()
2614− };
2615− line.push_str(&pad_right(&status, 4));
2616− }
2617 line.push(' ');
1866 line.push(' ');
2618 line.push_str(&t.id);
1867 line.push_str(&t.id);
2619− if bar_col {
1868+ if !t.bar.is_empty() {
2620 line.push(' ');
1869 line.push(' ');
2621− line.push_str(if t.bar.is_empty() { " " } else { &t.bar });
1870+ line.push_str(&t.bar);
2622 }
1871 }
2623 line.push_str(" ");
1872 line.push_str(" ");
2624− heads.push(line);
2625− }
2626− // the columns are placed by display width as drawn (an `icons` glyph is
2627− // two columns wide), from the message column on
2628− let msg_col = rows
2629− .iter()
2630− .zip(&heads)
2631− .filter(|(n, _)| n.commit().is_some())
2632− .map(|(_, h)| width(h))
2633− .max()
2634− .unwrap_or(0);
2635− let lab_w = texts.iter().map(|t| width(&t.labels)).max().unwrap_or(0);
2636− // the data columns, each as wide as its widest entry, so that the margin
2637− // after them starts at one offset too
2638− let mut extra_w: Vec<usize> = Vec::new();
2639− for t in &texts {
2640− for (k, e) in t.meta_extra.iter().enumerate() {
2641− if k == extra_w.len() {
2642− extra_w.push(0);
2643− }
2644− extra_w[k] = extra_w[k].max(width(e));
2645− }
2646− }
2647− // the margin's age and initials, each as wide as its widest entry, so
2648− // that a `13m` among `9m`s or a single initial moves nothing
2649− let margin_w = (
2650− texts.iter().map(|t| width(&t.age)).max().unwrap_or(0),
2651− texts.iter().map(|t| width(&t.initials)).max().unwrap_or(0),
2652− );
2653− let metas: Vec<String> = texts
2654− .iter()
2655− .map(|t| meta_block(t, &extra_w, margin_w, opts, pal))
2656− .collect();
2657− // message truncation to the terminal width (§Step 4): the widest part
2658− // right of the message column (labels, data, margin) sets the room left
2659− // for every message, and only messages are cut to it, so the label and
2660− // margin columns hold
2661− if let Some(w) = term_w {
2662− let right = texts
2663− .iter()
2664− .zip(&metas)
2665− .map(|(t, m)| {
2666− if !m.is_empty() {
2667− (if label_col { 2 + lab_w } else { 0 }) + 2 + width(m)
2668− } else if !t.labels.is_empty() {
2669− 2 + width(&t.labels)
2670− } else {
2671− 0
2672− }
2673− })
2674− .max()
2675− .unwrap_or(0);
2676− let room = w.saturating_sub(msg_col + right);
2677− for t in texts.iter_mut() {
2678− t.msg = cut_to_width(&t.msg, room);
2679− }
2680− }
2681− let msg_w = texts.iter().map(|t| width(&t.msg)).max().unwrap_or(0);
2682− let lab_off = msg_col + msg_w + 2;
2683− let meta_off = if label_col { lab_off + lab_w + 2 } else { lab_off };
2684− let mut lines: Vec<String> = Vec::new();
2685− // Detail lines use the state below their row. Advance it once per row,
2686− // as above, rather than replaying the preceding history.
2687− rail_state.fill(None);
2688− for (idx, n) in rows.iter().enumerate() {
2689− advance_rail_state(&mut rail_state, n, &placements[idx]);
2690− let t = &texts[idx];
2691− let mut line = std::mem::take(&mut heads[idx]);
2692− if n.commit().is_some() {
2693− pad_to(&mut line, msg_col);
2694− }
2695 line.push_str(&t.msg);
1873 line.push_str(&t.msg);
2696 if label_col && !t.labels.is_empty() {
1874 if label_col && !t.labels.is_empty() {
2697− pad_to(&mut line, lab_off);
1875+ let want = 2 + msg_end_max.saturating_sub(width(&t.msg));
1876+ line.push_str(&" ".repeat(want));
2698− line.push_str(&pal.label(&t.labels));
1877+ line.push_str(&pal.green(&pad_right(&t.labels, width(&t.labels))));
2699− }
1878+ }
1879+ // metadata block: extra columns (date / files / author) then the margin
1880+ // (age + initials), right-aligned together past the message+labels edge
1881+ let has_margin = opts.margin && !t.age.is_empty();
2700− if !metas[idx].is_empty() {
1882+ if !t.meta_extra.is_empty() || has_margin {
2701− pad_to(&mut line, meta_off);
1883+ let cur = if label_col && !t.labels.is_empty() {
1884+ msg_end_max + 2 + width(&t.labels)
1885+ } else {
1886+ width(&t.msg)
1887+ };
1888+ let want = 2 + lab_end_max.saturating_sub(cur);
1889+ line.push_str(&" ".repeat(want));
1890+ let mut meta: Vec<String> = Vec::new();
1891+ for e in &t.meta_extra {
1892+ meta.push(pal.grey(4, e));
1893+ }
1894+ if has_margin {
1895+ meta.push(pal.grey(4, &t.age));
1896+ meta.push(pal.author(&t.initials_author, &t.initials));
1897+ }
2702− line.push_str(&metas[idx]);
1898+ line.push_str(&meta.join(" "));
2703 }
1899 }
2704− // Remove layout padding, while preserving spaces in detail paths below.
1900+ let _ = pl;
2705− while line.ends_with(' ') { line.pop(); }
2706 // the whole focus row is highlighted with a background band across the
1901 // the whole focus row is highlighted with a background band across the
2707 // full terminal width (§Colour; colour only — the ▶ gutter still marks
1902 // full terminal width (§Colour; colour only — the ▶ gutter still marks
2708 // the focus when colour is off)
1903 // the focus when colour is off)
2709 let is_focus_row = n.commit().map(|c| c.is_focus).unwrap_or(false);
1904 let is_focus_row = n.commit().map(|c| c.is_focus).unwrap_or(false);
2710 if is_focus_row && pal.on {
1905 if is_focus_row && pal.on {
1906+ let target_w = if crate::show::stdout_is_tty() {
1907+ crate::show::terminal_width()
1908+ } else {
1909+ None
1910+ };
2711− let padded = match term_w {
1911+ let padded = match target_w {
2712 Some(w) if width(&line) < w => {
1912 Some(w) if width(&line) < w => {
2713 format!("{}{}", line, " ".repeat(w - width(&line)))
1913 format!("{}{}", line, " ".repeat(w - width(&line)))
2714 }
1914 }
2715 _ => line.clone(),
1915 _ => line.clone(),
2716 };
1916 };
2717− lines.push(pal.bg_line("97;48;5;17", &padded));
1917+ lines.push(pal.bg_line("48;5;236", &padded));
2718 } else {
1918 } else {
2719 lines.push(line);
1919 lines.push(line);
2720 }
1920 }
@@ -2724,14 +1924,12 @@ fn draw_rows(
2724 && n.commit().map(|c| c.is_focus && !c.detail_marks.is_empty()).unwrap_or(false)
1924 && n.commit().map(|c| c.is_focus && !c.detail_marks.is_empty()).unwrap_or(false)
2725 {
1925 {
2726 let c = n.commit().unwrap();
1926 let c = n.commit().unwrap();
2727− let (dchars, dlane0) = detail_rail_row(&rail_state);
1927+ let (dchars, dlane0) = detail_rail_row(rows, placements, idx, lanes_n);
2728 let mut dline = String::new();
1928 let mut dline = String::new();
2729 dline.push_str(" ");
1929 dline.push_str(" ");
2730 dline.push_str(&color_rails(&dchars, &dlane0, pal, None));
1930 dline.push_str(&color_rails(&dchars, &dlane0, pal, None));
2731− if overflow { dline.push_str(" "); }
2732 dline.push(' ');
1931 dline.push(' ');
2733− // the marks start two columns into the id column (§Step 4)
2734− dline.push_str(" ");
1932+ dline.push_str(&" ".repeat(id_w + 2));
2735 let marks: Vec<String> = c
1933 let marks: Vec<String> = c
2736 .detail_marks
1934 .detail_marks
2737 .iter()
1935 .iter()
@@ -2747,18 +1945,25 @@ fn draw_rows(
2747 format!("{} {}", colored, p)
1945 format!("{} {}", colored, p)
2748 })
1946 })
2749 .collect();
1947 .collect();
2750− let detail_lines = match term_w {
1948+ dline.push_str(&marks.join(" "));
2751− Some(w) => wrap_detail_items(&marks, w.saturating_sub(width(&dline)).max(1)),
1949+ lines.push(dline);
2752− None => vec![marks.join(" ")],
1950+ }
1951+ }
1952+ // message truncation to the terminal width (§Step 4)
1953+ if crate::show::stdout_is_tty() {
1954+ if let Some(w) = crate::show::terminal_width() {
1955+ truncate_lines(&mut lines, w, msg_off, label_col, opts.margin);
1956+ }
2753− };
1957+ }
2754− for text in detail_lines {
1958+ // no trailing whitespace on any row
2755− lines.push(format!("{}{}", dline, text));
1959+ for l in lines.iter_mut() {
2756− }
1960+ while l.ends_with(' ') {
1961+ l.pop();
2757 }
1962 }
2758 }
1963 }
2759 // Legend (§Legend): explain the symbols that actually appear, faintly, on
1964 // Legend (§Legend): explain the symbols that actually appear, faintly, on
2760 // the right of the tree if it fits the terminal width, else at the bottom.
1965 // the right of the tree if it fits the terminal width, else at the bottom.
2761− append_legend(&mut lines, rows, opts, pal, term_w, overflow);
1966+ append_legend(&mut lines, rows, opts, pal);
2762 let mut out = lines.join("\n");
1967 let mut out = lines.join("\n");
2763 out.push('\n');
1968 out.push('\n');
2764 Ok(out)
1969 Ok(out)
@@ -2771,11 +1976,10 @@ struct UsedSymbols {
2771 run: bool, // a ╎ n run row
1976 run: bool, // a ╎ n run row
2772 collapsed: bool, // a ⋯ n collapsed node
1977 collapsed: bool, // a ⋯ n collapsed node
2773 focus_gutter: bool, // ▶ present (a focus row exists)
1978 focus_gutter: bool, // ▶ present (a focus row exists)
2774− overflow: bool, // separate » status column
2775 detail_marks: Vec<char>, // + ~ − ✖ present in the focus's detail line
1979 detail_marks: Vec<char>, // + ~ − ✖ present in the focus's detail line
2776}
1980}
2777
1981
2778−fn collect_used(rows: &[&Display], opts: &TreeOptions, icons: bool, overflow: bool) -> UsedSymbols {
1982+fn collect_used(rows: &[&Display], opts: &TreeOptions, icons: bool) -> UsedSymbols {
2779 let mut present: BTreeSet<&'static str> = BTreeSet::new();
1983 let mut present: BTreeSet<&'static str> = BTreeSet::new();
2780 let mut run = false;
1984 let mut run = false;
2781 let mut collapsed = false;
1985 let mut collapsed = false;
@@ -2814,7 +2018,6 @@ fn collect_used(rows: &[&Display], opts: &TreeOptions, icons: bool, overflow: bo
2814 collapsed,
2018 collapsed,
2815 focus_gutter,
2019 focus_gutter,
2816 detail_marks: marks.into_iter().collect(),
2020 detail_marks: marks.into_iter().collect(),
2817− overflow,
2818 }
2021 }
2819}
2022}
2820
2023
@@ -2850,9 +2053,6 @@ fn legend_lines(used: &UsedSymbols) -> Vec<String> {
2850 if used.focus_gutter {
2053 if used.focus_gutter {
2851 out.push("▶ current commit".to_string());
2054 out.push("▶ current commit".to_string());
2852 }
2055 }
2853− if used.overflow {
2854− out.push("» branch exceeds lane limit; increase lanes".to_string());
2855− }
2856 if used.run {
2056 if used.run {
2857 out.push("╎ n run of n commits".to_string());
2057 out.push("╎ n run of n commits".to_string());
2858 }
2058 }
@@ -2869,15 +2069,8 @@ fn legend_lines(used: &UsedSymbols) -> Vec<String> {
2869/// faint, to the right of a tree row starting one row from the top, past the
2069/// faint, to the right of a tree row starting one row from the top, past the
2870/// widest tree line. If that would exceed the terminal width (or there is no
2070/// widest tree line. If that would exceed the terminal width (or there is no
2871/// terminal), put the legend at the bottom instead.
2071/// terminal), put the legend at the bottom instead.
2872−fn append_legend(
2873− lines: &mut Vec<String>,
2072+fn append_legend(lines: &mut Vec<String>, rows: &[&Display], opts: &TreeOptions, pal: &Palette) {
2874− rows: &[&Display],
2875− opts: &TreeOptions,
2876− pal: &Palette,
2877− tty_width: Option<usize>,
2878− overflow: bool,
2879−) {
2880− let used = collect_used(rows, opts, opts.icons, overflow);
2073+ let used = collect_used(rows, opts, opts.icons);
2881 let entries = legend_lines(&used);
2074 let entries = legend_lines(&used);
2882 if entries.is_empty() {
2075 if entries.is_empty() {
2883 return;
2076 return;
@@ -2886,6 +2079,11 @@ fn append_legend(
2886 let legend_w = entries.iter().map(|e| width(e)).max().unwrap_or(0);
2079 let legend_w = entries.iter().map(|e| width(e)).max().unwrap_or(0);
2887 let gap = 4;
2080 let gap = 4;
2888 let right_total = tree_w + gap + legend_w;
2081 let right_total = tree_w + gap + legend_w;
2082+ let tty_width = if crate::show::stdout_is_tty() {
2083+ crate::show::terminal_width()
2084+ } else {
2085+ None
2086+ };
2889 let fits_right = tty_width.map_or(false, |w| right_total <= w);
2087 let fits_right = tty_width.map_or(false, |w| right_total <= w);
2890 if fits_right {
2088 if fits_right {
2891 // pad every tree line to tree_w, then append the legend entries dim
2089 // pad every tree line to tree_w, then append the legend entries dim
@@ -2913,37 +2111,50 @@ fn append_legend(
2913 }
2111 }
2914}
2112}
2915
2113
2916−/// Update the rails above a row to the rails below it. Each occupied lane
2114+/// rails state below row `upto` (exclusive): which lanes hold rails, and which
2917−/// records whether it belongs to lane 0.
2115+/// of those are lane-0
2918−fn advance_rail_state(state: &mut [Option<bool>], n: &Display, p: &Placement) {
2116+fn rail_state_below(
2117+ rows: &[&Display],
2118+ placements: &[Placement],
2119+ upto: usize,
2120+ lanes_n: usize,
2121+) -> Vec<Option<bool>> {
2122+ let mut state: Vec<Option<bool>> = vec![None; lanes_n]; // Some(lane==0)
2123+ for (j, m) in rows.iter().enumerate().take(upto) {
2124+ let p = &placements[j];
2919− if let Some((src, clears)) = p.fork {
2125+ if let Some((src, clears)) = p.fork {
2920− if clears {
2126+ if clears {
2127+ state[src] = None;
2128+ }
2129+ }
2130+ if let Some(l) = p.lane {
2131+ state[l] = if m.children().is_empty() {
2921− state[src] = None;
2132+ None
2133+ } else {
2134+ Some(l == 0)
2135+ };
2136+ }
2137+ for &(l, _) in &p.reservations {
2138+ state[l] = Some(false);
2922 }
2139 }
2923 }
2140 }
2924− if let Some(l) = p.ends {
2925− state[l] = None;
2141+ state
2926− }
2927− if let Some(l) = p.lane {
2928− state[l] = if n.children().is_empty() { None } else { Some(l == 0) };
2929− }
2930− for &(l, _) in &p.reservations {
2931− state[l] = Some(false);
2932− }
2933}
2142}
2934
2143
2935−/// Rails characters for one node's row (§Step 4, the character table).
2144+/// rails characters for the row of node `idx` (§Step 4, the character table).
2936/// Returns the characters and, per character, whether it belongs to lane 0.
2145/// Returns the characters and, per character, whether it belongs to lane 0.
2937−/// On a run row they go on past the rails area when its count does not fit.
2938fn rail_row(
2146fn rail_row(
2939− n: &Display,
2147+ rows: &[&Display],
2940− pl: &Placement,
2148+ placements: &[Placement],
2941− state: &[Option<bool>],
2149+ idx: usize,
2942 lanes_n: usize,
2150 lanes_n: usize,
2943 opts: &TreeOptions,
2151 opts: &TreeOptions,
2944) -> (Vec<char>, Vec<bool>) {
2152) -> (Vec<char>, Vec<bool>) {
2153+ let n = rows[idx];
2154+ let pl = &placements[idx];
2945 let mut chars = vec![' '; 2 * lanes_n];
2155 let mut chars = vec![' '; 2 * lanes_n];
2946 let mut lane0 = vec![false; 2 * lanes_n];
2156 let mut lane0 = vec![false; 2 * lanes_n];
2157+ let state = rail_state_below(rows, placements, idx, lanes_n);
2947 // horizontal segments: (start, end) in lane coordinates
2158 // horizontal segments: (start, end) in lane coordinates
2948 let mut segments: Vec<(usize, usize)> = Vec::new();
2159 let mut segments: Vec<(usize, usize)> = Vec::new();
2949 if let Some((src, _)) = pl.fork {
2160 if let Some((src, _)) = pl.fork {
@@ -2987,25 +2198,19 @@ fn rail_row(
2987 }
2198 }
2988 } else if state[i].is_some() {
2199 } else if state[i].is_some() {
2989 '│'
2200 '│'
2201+ } else if pl.flattened && i == lanes_n - 1 {
2202+ '»'
2990 } else {
2203 } else {
2991 ' '
2204 ' '
2992 };
2205 };
2993 chars[2 * i] = c;
2206 chars[2 * i] = c;
2994 lane0[2 * i] = i == 0;
2207 lane0[2 * i] = i == 0;
2995 }
2208 }
2996− // on a run row the count follows ╎, or the rightmost rail drawn right of
2209+ // on a run row the count follows ╎, written into the rails area
2997− // it, so that it covers none; it is written into the rails area and, if
2998− // needed, on through the space after it into the blank id column. A run
2999− // reserves no lane and any fork on its row comes from its left, so the
3000− // rightmost character drawn so far is ╎ or such a rail.
3001 if let (Display::Run { count, .. }, Some(l)) = (n, pl.lane) {
2210 if let (Display::Run { count, .. }, Some(l)) = (n, pl.lane) {
3002− let from = chars.iter().rposition(|&c| c != ' ').unwrap_or(2 * l) + 1;
3003 for (k, d) in format!(" {}", count).chars().enumerate() {
2211 for (k, d) in format!(" {}", count).chars().enumerate() {
3004− let pos = from + k;
2212+ let pos = 2 * l + 1 + k;
3005− if pos == chars.len() {
2213+ if pos < chars.len() {
3006− chars.push(d);
3007− lane0.push(l == 0);
3008− } else {
3009 chars[pos] = d;
2214 chars[pos] = d;
3010 lane0[pos] = l == 0;
2215 lane0[pos] = l == 0;
3011 }
2216 }
@@ -3023,8 +2228,13 @@ fn rail_row(
3023
2228
3024/// detail line (§Step 4): `│` in every lane that holds a rail below the focus
2229/// detail line (§Step 4): `│` in every lane that holds a rail below the focus
3025/// row, including the focus's own lane if it has children
2230/// row, including the focus's own lane if it has children
3026−fn detail_rail_row(state: &[Option<bool>]) -> (Vec<char>, Vec<bool>) {
2231+fn detail_rail_row(
3027− let lanes_n = state.len();
2232+ rows: &[&Display],
2233+ placements: &[Placement],
2234+ idx: usize,
2235+ lanes_n: usize,
2236+) -> (Vec<char>, Vec<bool>) {
2237+ let state = rail_state_below(rows, placements, idx + 1, lanes_n);
3028 let mut chars = vec![' '; 2 * lanes_n];
2238 let mut chars = vec![' '; 2 * lanes_n];
3029 let mut lane0 = vec![false; 2 * lanes_n];
2239 let mut lane0 = vec![false; 2 * lanes_n];
3030 for i in 0..lanes_n {
2240 for i in 0..lanes_n {
@@ -3037,10 +2247,8 @@ fn detail_rail_row(state: &[Option<bool>]) -> (Vec<char>, Vec<bool>) {
3037}
2247}
3038
2248
3039/// Colour the rails. Rail connectors are lane-coloured (lane 0 = immutable
2249/// Colour the rails. Rail connectors are lane-coloured (lane 0 = immutable
3040−/// bright blue, others rotating accent hues); the node glyph at `glyph`
2250+/// blue, others dim); the node glyph at `glyph` (its lane position and ANSI
3041−/// (its lane position and ANSI code) is meaning-coloured. A glyph two
2251+/// code) is meaning-coloured.
3042−/// columns wide (`icons`) fills both
3043−/// characters of its lane, so the one after it is not drawn (§Glyphs).
3044fn color_rails(
2252fn color_rails(
3045 chars: &[char],
2253 chars: &[char],
3046 lane0: &[bool],
2254 lane0: &[bool],
@@ -3048,12 +2256,7 @@ fn color_rails(
3048 glyph: Option<(usize, &str)>,
2256 glyph: Option<(usize, &str)>,
3049) -> String {
2257) -> String {
3050 let mut s = String::new();
2258 let mut s = String::new();
3051− let mut covered = false;
3052 for (i, c) in chars.iter().enumerate() {
2259 for (i, c) in chars.iter().enumerate() {
3053− if std::mem::take(&mut covered) {
3054− continue;
3055− }
3056− covered = UnicodeWidthChar::width(*c) == Some(2);
3057 let t = c.to_string();
2260 let t = c.to_string();
3058 if *c == ' ' {
2261 if *c == ' ' {
3059 s.push(*c);
2262 s.push(*c);
@@ -3063,148 +2266,123 @@ fn color_rails(
3063 } else if lane0[i] {
2266 } else if lane0[i] {
3064 s.push_str(&pal.blue(&t));
2267 s.push_str(&pal.blue(&t));
3065 } else {
2268 } else {
3066− s.push_str(&pal.rail(i / 2, &t));
2269+ s.push_str(&pal.dim(&t));
3067 }
2270 }
3068 } else if lane0[i] {
2271 } else if lane0[i] {
3069 s.push_str(&pal.blue(&t));
2272 s.push_str(&pal.blue(&t));
3070 } else {
2273 } else {
3071− s.push_str(&pal.rail(i / 2, &t));
2274+ s.push_str(&pal.dim(&t));
3072 }
2275 }
3073 }
2276 }
3074 s
2277 s
3075}
2278}
3076
2279
3077−/// Wrap without dropping text or splitting ANSI sequences. Close and reopen
2280+/// Cut the message so labels and the margin keep their columns (§Step 4).
3078−/// active styles at row boundaries so continuation prefixes stay unstyled.
2281+/// Only applied on a tty, so it is exercised directly by the tests.
3079−fn wrap_display_text(text: &str, limit: usize) -> Vec<String> {
2282+pub fn truncate_lines(
3080− let limit = limit.max(1);
3081− let mut lines = Vec::new();
3082− let mut line = String::new();
2283+ lines: &mut [String],
3083− let mut used = 0;
2284+ term_w: usize,
3084− let mut active = String::new();
2285+ msg_off: usize,
3085− let mut chars = text.chars();
2286+ label_col: bool,
3086− while let Some(c) = chars.next() {
2287+ margin: bool,
3087− if c == '\x1b' {
2288+) {
3088− let mut escape = String::from(c);
2289+ for line in lines.iter_mut() {
3089− for e in chars.by_ref() {
2290+ if width(line) <= term_w {
3090− escape.push(e);
3091− if e == 'm' { break; }
3092− }
3093− if escape == "\x1b[0m" { active.clear(); } else { active.push_str(&escape); }
3094− line.push_str(&escape);
3095 continue;
2291 continue;
3096 }
2292 }
3097− let cells = UnicodeWidthChar::width(c).unwrap_or(0);
2293+ // find the message span: it starts at msg_off and ends where the
3098− if used > 0 && used + cells > limit {
2294+ // label/margin padding (2+ spaces) begins
3099− if !active.is_empty() { line.push_str("\x1b[0m"); }
2295+ let plain = strip_ansi(line);
3100− lines.push(line);
2296+ if width(&plain) <= term_w {
3101− line = active.clone();
3102− used = 0;
3103− }
3104− line.push(c);
3105− used += cells;
3106− }
3107− if !active.is_empty() { line.push_str("\x1b[0m"); }
3108− lines.push(line);
3109− lines
3110−}
3111−
3112−/// Keep whole detail items together when possible; split only an item that
3113−/// cannot fit on an otherwise empty continuation row.
3114−fn wrap_detail_items(items: &[String], limit: usize) -> Vec<String> {
3115− let mut lines = Vec::new();
3116− let mut line = String::new();
3117− for item in items {
3118− if !line.is_empty() && width(&line) + 3 + width(item) <= limit {
3119− line.push_str(" ");
3120− line.push_str(item);
3121 continue;
2297 continue;
3122 }
2298 }
3123− if !line.is_empty() { lines.push(std::mem::take(&mut line)); }
2299+ // walk characters, tracking display column. Walk the *plain* string:
3124− let mut parts = wrap_display_text(item, limit);
2300+ // skipping only the ESC character would leave the rest of each escape
3125− line = parts.pop().unwrap_or_default();
2301+ // sequence (`[1;36m`) counted as display width.
3126− lines.extend(parts);
2302+ let mut col = 0usize;
2303+ let mut cut_at: Option<usize> = None;
2304+ let mut tail_start: Option<usize> = None;
2305+ let mut prev_two_spaces = 0usize;
2306+ for (bi, ch) in plain.char_indices() {
2307+ let w = UnicodeWidthChar::width(ch).unwrap_or(0);
2308+ if col >= msg_off {
2309+ if ch == ' ' {
2310+ prev_two_spaces += 1;
2311+ } else {
2312+ prev_two_spaces = 0;
2313+ }
2314+ if prev_two_spaces == 2 && tail_start.is_none() {
2315+ tail_start = Some(bi);
2316+ }
2317+ }
2318+ if col + w >= term_w && cut_at.is_none() {
2319+ cut_at = Some(bi);
2320+ }
2321+ col += w;
2322+ }
2323+ if !label_col && !margin {
2324+ continue;
2325+ }
2326+ if let (Some(_cut), Some(_tail)) = (cut_at, tail_start) {
2327+ // rebuild from the plain string: this path only matters on a tty,
2328+ // so keep it simple and operate on plain text
2329+ let p = &plain;
2330+ let pw = width(p);
2331+ if pw <= term_w {
2332+ continue;
2333+ }
2334+ // find message end (last run of 2+ spaces that starts the fixed tail)
2335+ let bytes = p.as_bytes();
2336+ let mut tail_bi = p.len();
2337+ let mut i = 0usize;
2338+ while i + 1 < bytes.len() {
2339+ if bytes[i] == b' ' && bytes[i + 1] == b' ' {
2340+ // candidate: is everything after this point the fixed tail?
2341+ tail_bi = i;
2342+ }
2343+ i += 1;
2344+ }
2345+ let tail = &p[tail_bi..];
2346+ let tail_w = width(tail);
2347+ let head_budget = term_w.saturating_sub(tail_w + 1);
2348+ // the whole plain line; the loop below stops it at head_budget.
2349+ // (This used to slice by `char_indices().count()`, a *char* count
2350+ // used as a *byte* index — a panic on any line whose multi-byte
2351+ // characters put that index inside a character.)
2352+ let head = p.as_str();
2353+ let mut head_w = 0usize;
2354+ let mut head_end = 0usize;
2355+ for (bi, ch) in head.char_indices() {
2356+ let cw = UnicodeWidthChar::width(ch).unwrap_or(0);
2357+ if head_w + cw > head_budget {
2358+ break;
2359+ }
2360+ head_w += cw;
2361+ head_end = bi + ch.len_utf8();
2362+ }
2363+ let head = head[..head_end].trim_end();
2364+ *line = format!("{}…{}", head, tail);
2365+ }
3127 }
2366 }
3128− if !line.is_empty() { lines.push(line); }
3129− lines
3130}
2367}
3131
2368
3132−/// `s` cut to display width `w`, ending with `…` when anything is cut (and
3133−/// there is a column for it): the message on a terminal (§Step 4). Colour
3134−/// escapes are kept whole and not counted, and those past the cut still
3135−/// follow it, so every style `s` opens is closed.
3136−fn cut_to_width(s: &str, w: usize) -> String {
2369+fn strip_ansi(s: &str) -> String {
3137− if width(s) <= w {
3138− return s.to_string();
3139− }
3140− let mut kept = String::new();
3141− // escapes not yet followed by a kept character
3142− let mut pending = String::new();
2370+ let mut out = String::new();
3143− let mut used = 0usize;
2371+ let mut esc = false;
3144− let mut in_esc = false;
3145− let mut full = false;
3146 for c in s.chars() {
2372 for c in s.chars() {
3147− if c == '\x1b' {
2373+ if esc {
3148− in_esc = true;
2374+ if c == 'm' {
3149− }
2375+ esc = false;
3150− if in_esc {
3151− in_esc = c != 'm';
3152− pending.push(c);
3153− continue;
3154− }
2376+ }
3155− if full {
3156 continue;
2377 continue;
3157 }
2378 }
3158− // one column is left for the `…`
2379+ if c == '\x1b' {
3159− let cw = UnicodeWidthChar::width(c).unwrap_or(0);
3160− if used + cw >= w {
3161− full = true;
2380+ esc = true;
3162 continue;
2381 continue;
3163 }
2382 }
3164− kept.push_str(&pending);
3165− pending.clear();
3166− kept.push(c);
2383+ out.push(c);
3167− used += cw;
3168− }
3169− let ellipsis = if w == 0 { "" } else { "…" };
3170− format!("{}{}{}", kept.trim_end_matches(' '), ellipsis, pending)
3171−}
3172−
3173−/// Pad `line` with spaces to display column `col`.
3174−fn pad_to(line: &mut String, col: usize) {
3175− let w = width(line);
3176− if w < col {
3177− line.push_str(&" ".repeat(col - w));
3178 }
2384 }
3179−}
2385+ out
3180−
3181−/// A row's metadata block (§Step 4): the data columns (date / files /
3182−/// author), each padded to its column's width in `extra_w`, then the margin
3183−/// (age and initials), each right-aligned in its width in `margin_w`. Empty
3184−/// when the row shows none of them.
3185−fn meta_block(
3186− t: &RowText,
3187− extra_w: &[usize],
3188− margin_w: (usize, usize),
3189− opts: &TreeOptions,
3190− pal: &Palette,
3191−) -> String {
3192− let mut meta: Vec<String> = Vec::new();
3193− for (e, w) in t.meta_extra.iter().zip(extra_w) {
3194− let mut s = if e.is_empty() { String::new() } else { pal.grey(4, e) };
3195− s.push_str(&" ".repeat(w - width(e)));
3196− meta.push(s);
3197− }
3198− if opts.margin && !t.age.is_empty() {
3199− let (age_w, init_w) = margin_w;
3200− let mut age = " ".repeat(age_w - width(&t.age));
3201− age.push_str(&pal.grey(4, &t.age));
3202− meta.push(age);
3203− let mut init = " ".repeat(init_w - width(&t.initials));
3204− init.push_str(&pal.author(&t.initials_author, &t.initials));
3205− meta.push(init);
3206− }
3207− meta.join(" ").trim_end().to_string()
3208}
2386}
3209
2387
3210fn glyph_for(info: &CommitInfo, icons: bool) -> &'static str {
2388fn glyph_for(info: &CommitInfo, icons: bool) -> &'static str {
@@ -3244,18 +2422,20 @@ fn glyph_for(info: &CommitInfo, icons: bool) -> &'static str {
3244/// The ANSI colour code for a node glyph, by meaning (§Colour: glyphs carry
2422/// The ANSI colour code for a node glyph, by meaning (§Colour: glyphs carry
3245/// meaning; colour only adds emphasis). Mirrors the precedence of `glyph_for`.
2423/// meaning; colour only adds emphasis). Mirrors the precedence of `glyph_for`.
3246fn glyph_ansi(info: &CommitInfo) -> &'static str {
2424fn glyph_ansi(info: &CommitInfo) -> &'static str {
3247− node_ansi(glyph_for(info, false))
2425+ if info.id == ROOT_ID {
3248−}
3249−
3250−fn node_ansi(glyph: &str) -> &'static str {
3251− match glyph {
3252− "⌂" => "1;95", // root: bold magenta
2426+ "1" // root: bold
2427+ } else if info.conflict {
3253− "⊗" => "91", // conflict: bright red
2428+ "31" // conflict: red
2429+ } else if info.empty {
3254− "◌" => "2", // empty: dim
2430+ "2" // empty: dim
2431+ } else if info.immutable {
3255− "◆" => "94", // immutable: bright blue
2432+ "34" // immutable: blue
2433+ } else if info.is_focus {
3256− "◉" => "1;96", // focus: bold cyan
2434+ "1;36" // focus: bold cyan
2435+ } else if info.is_ancestor_of_focus {
3257− "●" => "92", // ancestor of focus: bright green
2436+ "32" // ancestor of focus: green
2437+ } else {
3258− _ => "97", // other commit: bright white
2438+ "37" // other commit: light grey
3259 }
2439 }
3260}
2440}
3261
2441
@@ -3263,17 +2443,16 @@ fn size_bar(size: Option<usize>, pal: &Palette) -> String {
3263 match size {
2443 match size {
3264 None | Some(0) => String::new(),
2444 None | Some(0) => String::new(),
3265 Some(n) => {
2445 Some(n) => {
3266− // one glyph per threshold 1, 10, 50, 200, 1000 (§7.11 column 5)
3267− let (bar, level) = if n >= SIZE_BAR_TOP {
2446+ let (bar, level) = if n >= 1000 {
3268 ("▇", 4)
2447 ("▇", 4)
3269 } else if n >= 200 {
2448 } else if n >= 200 {
3270− ("▅", 3)
2449+ ("▇", 3)
3271 } else if n >= 50 {
2450 } else if n >= 50 {
3272− ("▃", 2)
2451+ ("▅", 2)
3273 } else if n >= 10 {
2452 } else if n >= 10 {
3274− ("▂", 1)
2453+ ("▃", 1)
3275 } else {
2454 } else {
3276− ("▁", 0)
2455+ ("▂", 0)
3277 };
2456 };
3278 pal.accent(level, bar)
2457 pal.accent(level, bar)
3279 }
2458 }
@@ -3287,280 +2466,3 @@ fn initials(name: &str) -> String {
3287 .collect::<String>()
2466 .collect::<String>()
3288 .to_lowercase()
2467 .to_lowercase()
3289}
2468}
3290−
3291−#[cfg(test)]
3292−mod tests {
3293− use super::*;
3294− use crate::value::BlobVal;
3295− use std::collections::HashMap;
3296−
3297− /// Lines deleted plus inserted by a shortest diff of `a` to `b`, uncapped
3298− /// and found without anything `changed_lines` uses: both lengths less
3299− /// twice a longest common subsequence, which Hyyrö's bit-parallel LCS
3300− /// finds a machine word of `a` at a time.
3301− fn exact(a: &[String], b: &[String]) -> usize {
3302− let words = a.len().div_ceil(64);
3303− // per line of `a`, a bit at each place it occurs in `a`
3304− let mut masks: HashMap<&str, Vec<u64>> = HashMap::new();
3305− for (i, l) in a.iter().enumerate() {
3306− masks.entry(l.as_str()).or_insert_with(|| vec![0; words])[i / 64] |= 1 << (i % 64);
3307− }
3308− let absent = vec![0; words];
3309− // after each line of `b`, the zero bits of `v` up to a place in `a`
3310− // count a longest common subsequence of `a` up to there and `b` so
3311− // far; the bits past the end of `a` stay ones
3312− let mut v = vec![u64::MAX; words];
3313− for l in b {
3314− let m = masks.get(l.as_str()).unwrap_or(&absent);
3315− let mut carry = false;
3316− for (v, m) in v.iter_mut().zip(m) {
3317− let (sum, c1) = v.overflowing_add(*v & m);
3318− let (sum, c2) = sum.overflowing_add(carry as u64);
3319− carry = c1 || c2;
3320− *v = sum | (*v & !m);
3321− }
3322− }
3323− let common: usize = v.iter().map(|w| w.count_zeros() as usize).sum();
3324− a.len() + b.len() - 2 * common
3325− }
3326−
3327− /// `changed_lines` from `a` to `b` and back is the exact count, or the
3328− /// cap if that is less: under the size bar's cap, caps on either side of
3329− /// the first search's 64 rounds, `cap` if given, and the exact count plus
3330− /// one, which leaves no room: a bound or a search stopped short that
3331− /// overcounts at all returns the cap there.
3332− fn check(a: &[String], b: &[String], cap: Option<usize>) {
3333− let (va, vb) = (BlobVal::text_blob(&a.concat()), BlobVal::text_blob(&b.concat()));
3334− let want = exact(a, b);
3335− assert_eq!(want, exact(b, a));
3336− let mut caps = vec![1, 64, 65, want + 1, SIZE_BAR_TOP];
3337− caps.extend(cap);
3338− caps.retain(|c| *c <= SIZE_BAR_TOP);
3339− for cap in caps {
3340− for (from, to) in [(&va, &vb), (&vb, &va)] {
3341− let got = changed_lines(Some(from), Some(to), cap).unwrap();
3342− assert_eq!(got, want.min(cap), "cap {}, {} and {} lines", cap, a.len(), b.len());
3343− }
3344− }
3345− }
3346−
3347− /// xorshift64*: the same numbers on every run
3348− struct Rng(u64);
3349−
3350− impl Rng {
3351− fn below(&mut self, n: usize) -> usize {
3352− self.0 ^= self.0 >> 12;
3353− self.0 ^= self.0 << 25;
3354− self.0 ^= self.0 >> 27;
3355− (self.0.wrapping_mul(0x2545_f491_4f6c_dd1d) >> 32) as usize % n
3356− }
3357− }
3358−
3359− #[test]
3360− fn changed_lines_is_exact_up_to_its_cap() {
3361− // the size bar is drawn from this count (§7.11 column 5), and no
3362− // shortcut may change it, whatever the machine or the content.
3363− // A few lines repeating, disjoint adjacent pairs of them swapped, or
3364− // lines a whole number of periods apart each replaced by the line
3365− // after it (two lines each): no line occurs once on either side, and
3366− // every diagonal the period divides runs along equal lines from one
3367− // change to the next. Swapping leaves how many times each line
3368− // occurs as it was; replacing always turns the same line into the
3369− // same other one, so the counts alone bound it by its size
3370− for period in [2, 3] {
3371− let lines: Vec<String> = (0..3000).map(|i| format!("l{}\n", i % period)).collect();
3372− for changes in [1, 33, 499, 501] {
3373− let (mut swapped, mut replaced) = (lines.clone(), lines.clone());
3374− let step = 2990 / changes;
3375− let periods = step / period * period;
3376− for k in 0..changes {
3377− let at = 3 + step * k;
3378− swapped.swap(at, at + 1);
3379− let at = 3 + periods * k;
3380− replaced[at] = lines[at + 1].clone();
3381− }
3382− check(&lines, &swapped, None);
3383− check(&lines, &replaced, None);
3384− }
3385− }
3386− // runs of one line swapped: a shortest diff keeps the longer run
3387− let runs = |x: &str, n: usize, y: &str, m: usize| -> Vec<String> {
3388− let run = |l: &str, n| std::iter::repeat_n(format!("{}\n", l), n);
3389− run(x, n).chain(run(y, m)).collect()
3390− };
3391− check(&runs("a", 499, "b", 501), &runs("b", 501, "a", 500), None);
3392− check(&runs("a", 500, "b", 501), &runs("b", 501, "a", 500), None);
3393− // distinct lines, blocks moved past as many others: two lines per line
3394− // moved, where only the lines that occur once on each side bound it
3395− let lines: Vec<String> = (0..4000).map(|i| format!("line {}\n", i)).collect();
3396− for lens in [&[90; 5][..], &[100, 100, 100, 100, 99], &[100; 5], &[250, 251], &[600]] {
3397− let mut moved = lines.clone();
3398− for (j, len) in lens.iter().enumerate() {
3399− let at = 100 + 700 * j;
3400− let block: Vec<String> = moved.drain(at..at + len).collect();
3401− moved.splice(at + len..at + len, block);
3402− }
3403− check(&lines, &moved, None);
3404− }
3405− // random edits of random files, of two lines up to mostly distinct
3406− // ones: lines inserted, removed, replaced, and blocks moved
3407− let mut rng = Rng(0x9e37_79b9_7f4a_7c15);
3408− for _ in 0..40 {
3409− let distinct = [2, 5, 50, 1 << 20][rng.below(4)];
3410− let line = |rng: &mut Rng| format!("{}\n", rng.below(distinct));
3411− let a: Vec<String> = (0..rng.below(800)).map(|_| line(&mut rng)).collect();
3412− let mut b = a.clone();
3413− for _ in 0..rng.below(300) {
3414− let at = rng.below(b.len() + 1);
3415− match rng.below(4) {
3416− 0 => b.insert(at, line(&mut rng)),
3417− 1 if at < b.len() => {
3418− b.remove(at);
3419− }
3420− 2 if at < b.len() => b[at] = line(&mut rng),
3421− _ => {
3422− let end = (at + rng.below(40)).min(b.len());
3423− let block: Vec<String> = b.drain(at..end).collect();
3424− let to = rng.below(b.len() + 1);
3425− b.splice(to..to, block);
3426− }
3427− }
3428− }
3429− check(&a, &b, Some(1 + rng.below(SIZE_BAR_TOP)));
3430− }
3431− }
3432−
3433− #[test]
3434− fn banded_distance_is_exact_under_its_max() {
3435− // the search `line_distance` falls back on, checked on its own,
3436− // since `changed_lines` only reaches it where Myers' search would
3437− // take long. Under the exact count plus one the band has no room to
3438− // spare, so a word wrongly updated or left alone at its edges
3439− // overcounts there. Files of a few words, random or repeating a few
3440− // lines, with a band narrower than them under most caps
3441− let text = |ls: &[usize]| -> Vec<String> {
3442− ls.iter().map(|l| format!("{}\n", l)).collect()
3443− };
3444− let mut rng = Rng(0x2545_f491_4f6c_dd1d);
3445− for _ in 0..200 {
3446− let distinct = [1, 2, 3, 50][rng.below(4)];
3447− let most = [70, 300, 700][rng.below(3)];
3448− let len = rng.below(most);
3449− let periodic = rng.below(2) == 0;
3450− let a: Vec<usize> = (0..len)
3451− .map(|i| if periodic { i % distinct } else { rng.below(distinct) })
3452− .collect();
3453− let mut b = a.clone();
3454− for _ in 0..rng.below(60) {
3455− let at = rng.below(b.len() + 1);
3456− match rng.below(5) {
3457− 0 => b.insert(at, rng.below(distinct)),
3458− 1 if at < b.len() => {
3459− b.remove(at);
3460− }
3461− 2 if at < b.len() => b[at] = rng.below(distinct),
3462− 3 if at + 1 < b.len() => b.swap(at, at + 1),
3463− _ => {
3464− let end = (at + rng.below(40)).min(b.len());
3465− let block: Vec<usize> = b.drain(at..end).collect();
3466− let to = rng.below(b.len() + 1);
3467− b.splice(to..to, block);
3468− }
3469− }
3470− }
3471− let want = exact(&text(&a), &text(&b));
3472− for max in [1, 2, want + 1, want + 2, 64, 65, SIZE_BAR_TOP, 1 + rng.below(want + 2)] {
3473− for (x, y) in [(&a, &b), (&b, &a)] {
3474− let mut steps = 0;
3475− let got = banded_distance(x, y, distinct, max, &mut steps);
3476− assert_eq!(got, want.min(max), "max {}, {:?} and {:?}", max, x, y);
3477− assert!(steps <= banded_steps(x.len(), y.len(), max));
3478− }
3479− }
3480− }
3481− }
3482−
3483− #[test]
3484− fn line_distance_takes_steps_by_the_band_not_by_the_file() {
3485− // two lines alternating, 480 pairs of neighbours swapped: 960 lines.
3486− // Myers' search runs along equal lines on every other diagonal from
3487− // one swap to the next, a pass over the file for each of about 500
3488− // diagonals, and on such a file of a million lines drawing the size
3489− // bar took seconds. `line_distance` gives it up once it has taken as
3490− // many steps as the bit-parallel search takes at most, 18 words per
3491− // line under the size bar's cap, so it takes at most twice that and
3492− // one diagonal with its run, whatever the lines are
3493− let a: Vec<usize> = (0..100_000).map(|i| i % 2).collect();
3494− let mut b = a.clone();
3495− for k in 0..480 {
3496− b.swap(1000 + 200 * k, 1001 + 200 * k);
3497− }
3498− let (mut plain, mut steps) = (0, 0);
3499− assert_eq!(edit_distance(&a, &b, SIZE_BAR_TOP, usize::MAX, &mut plain), Some(960));
3500− assert_eq!(line_distance(&a, &b, 2, SIZE_BAR_TOP, &mut steps), 960);
3501− let most = banded_steps(a.len(), b.len(), SIZE_BAR_TOP);
3502− assert!(most <= 18 * WORD * a.len(), "{} steps at most", most);
3503− assert!(steps <= 2 * most + DIAGONAL + a.len(), "{} steps", steps);
3504− assert!(plain > 400 * a.len(), "{} steps", plain);
3505− }
3506−
3507− #[test]
3508− fn line_distance_gives_up_where_the_bit_parallel_search_costs_less() {
3509− // the hand-over is where it saves time, not where the two searches
3510− // have taken as many steps. Two lines alternating, 32 pairs of
3511− // neighbours swapped far apart: Myers' search compares about twice
3512− // as many pairs of lines as `banded_distance` takes word steps, but
3513− // a word step costs several comparisons, so it is the cheaper search
3514− // and finishes. Giving it up there made the search over a million
3515− // such lines take three times as long in a debug build
3516− let a: Vec<usize> = (0..100_000).map(|i| i % 2).collect();
3517− let mut b = a.clone();
3518− for k in 0..32 {
3519− b.swap(1001 + 3000 * k, 1002 + 3000 * k);
3520− }
3521− let (mut plain, mut steps) = (0, 0);
3522− assert_eq!(edit_distance(&a, &b, SIZE_BAR_TOP, usize::MAX, &mut plain), Some(64));
3523− assert_eq!(line_distance(&a, &b, 2, SIZE_BAR_TOP, &mut steps), 64);
3524− assert_eq!(steps, plain);
3525− // distinct lines, a block of 400 moved past 10000 others: Myers'
3526− // search reaches some 320000 diagonals, on most of which no line is
3527− // equal, and reaching one costs about twice a word step, so it gives
3528− // up and the two searches together take fewer steps than it alone
3529− let a: Vec<usize> = (0..10_400).collect();
3530− let b: Vec<usize> = (400..10_400).chain(0..400).collect();
3531− let (mut plain, mut steps) = (0, 0);
3532− assert_eq!(edit_distance(&a, &b, SIZE_BAR_TOP, usize::MAX, &mut plain), Some(800));
3533− assert_eq!(line_distance(&a, &b, a.len(), SIZE_BAR_TOP, &mut steps), 800);
3534− assert!(steps < plain, "{} steps, {} alone", steps, plain);
3535− }
3536−
3537− #[test]
3538− fn reorder_bound_keeps_no_line_more_often_than_either_side_has_it() {
3539− // the bound only spares `changed_lines` a search: the exact counts
3540− // above hold with any lower bound, however loose, so only this pins
3541− // how tight it is. Lines that occur once on each side keep at most a
3542− // longest sequence of them in the same order on both: ten reversed
3543− // keep one
3544− let distinct: Vec<usize> = (0..10).collect();
3545− let reversed: Vec<usize> = (0..10).rev().collect();
3546− assert_eq!(reorder_bound(&distinct, &reversed, 10), 18);
3547− // any other line is kept at most as many times as the side with
3548− // fewer of it has it: two lines alternating with 501 of the 1500
3549− // `0`s turned into `1`s keep 999 `0`s and 1500 `1`s. Counting them
3550− // only up to the shorter side kept all 3000, a bound of 0, and the
3551− // search ran over the whole file
3552− let alternating: Vec<usize> = (0..3000).map(|i| i % 2).collect();
3553− let mut replaced = alternating.clone();
3554− for k in 0..501 {
3555− replaced[2 * k] = 1;
3556− }
3557− assert_eq!(reorder_bound(&alternating, &replaced, 2), 1002);
3558− assert_eq!(reorder_bound(&replaced, &alternating, 2), 1002);
3559− // the two kinds side by side add up
3560− let then = |once: &[usize], others: &[usize]| -> Vec<usize> {
3561− once.iter().copied().chain(others.iter().map(|l| l + 10)).collect()
3562− };
3563− let (a, b) = (then(&distinct, &alternating), then(&reversed, &replaced));
3564− assert_eq!(reorder_bound(&a, &b, 12), 1020);
3565− }
3566−}
src/repo.rs+64−498
@@ -1,115 +1,27 @@
1//! Repo-zipper operations, the reference `by` walk (§10), and the
1//! Repo-zipper operations, the reference `by` walk (§10), and the
2//! persistence validation shared by `validate` and the jj backend (§7.5).
2//! persistence validation shared by `validate` and the jj backend (§7.5).
3
3
4−use crate::domain::{Backend, ROOT_ID};
4+use crate::domain::ROOT_ID;
5use crate::eval::Interp;
5use crate::eval::Interp;
6use crate::value::{value_eq, Crash, Value};
6use crate::value::{value_eq, Crash, Value};
7−use std::collections::{BTreeMap, BTreeSet, HashMap};
7+use std::collections::{BTreeMap, BTreeSet};
8
8
9/// navigate: refocus `repo` on the commit with change id `id` (§10 reference).
9/// navigate: refocus `repo` on the commit with change id `id` (§10 reference).
10−///
11−/// Finding the commit and refocusing onto it are separate walks: refocusing
12−/// builds a new location — a record, with the siblings split around a hole —
13−/// so doing it for every node visited on the way cost an allocation per
14−/// commit. Only the commits on the path to the target need one, and there are
15−/// as many of those as the history is deep.
16pub fn by_id(repo: &Value, id: &str) -> Result<Option<Value>, Crash> {
10pub fn by_id(repo: &Value, id: &str) -> Result<Option<Value>, Crash> {
17 let top = top_of(repo)?;
11 let top = top_of(repo)?;
18− if id_is(&top.field("root")?, id)? {
12+ let mut stack = vec![top];
19− return Ok(Some(top));
13+ while let Some(loc) = stack.pop() {
20− }
14+ let root_id = id_of(&loc.field("root")?)?;
21− let mut path = Vec::new();
15+ if root_id == id {
22− let children = top.field("children")?;
23− if !find_path(children.as_list()?, id, &mut path)? {
24− return Ok(None);
25− }
26− // Descend the path collecting one frame per level and assemble the
27− // location once. Refocusing step by step would rebuild the context at
28− // every level, which is quadratic in the depth on its own.
29− let mut root = top.field("root")?;
30− let mut children = top.field("children")?;
31− let mut frames: Vec<Value> = Vec::with_capacity(path.len());
32− for idx in path {
33− let kids = children.as_list()?;
34− let child = kids[idx].clone();
35− frames.push(Value::record(&[
36− ("left", Value::list(kids[..idx].to_vec())),
37− ("parent", root),
38− ("right", Value::list(kids[idx + 1..].to_vec())),
39− ]));
40− root = child.field("root")?;
41− children = child.field("children")?;
42− }
43− // innermost frame first: `up` reads context.first() as the parent
44− frames.reverse();
45− // `top_of` leaves no context above, but do not depend on that here
46− frames.extend_from_slice(top.field("context")?.as_list()?);
47− Ok(Some(Value::record(&[
48− ("children", children),
49− ("context", Value::list(frames)),
50− ("root", root),
51− ])))
52−}
53−
54−/// indices of the children to descend through to reach `id`, if it is there
55−fn find_path(children: &[Value], id: &str, path: &mut Vec<usize>) -> Result<bool, Crash> {
56− for (i, c) in children.iter().enumerate() {
57− path.push(i);
58− if id_is(&c.field("root")?, id)? {
59− return Ok(true);
16+ return Ok(Some(loc));
60 }
17 }
61− let grandchildren = c.field("children")?;
18+ let children = loc.field("children").and_then(|v| v.as_list().map(|x| x.to_vec()))?;
62− if find_path(grandchildren.as_list()?, id, path)? {
19+ // push children as refocused locations; iterate in order
63− return Ok(true);
20+ for child in children.iter() {
21+ stack.push(refocus_child(&loc, child)?);
64 }
22 }
65− path.pop();
66− }
67− Ok(false)
68−}
69−
70−thread_local! {
71− // One repository value, held while it is the key so its address cannot
72− // be reused. Edits create a different record and invalidate the index.
73− static COMMIT_INDEX: std::cell::RefCell<Option<(
74− std::rc::Rc<crate::value::RecordMap>, Option<HashMap<String, Value>>,
75− )>> = const { std::cell::RefCell::new(None) };
76−}
77−
78−/// The root of `by_id`, without allocating a zipper for every queried id.
79−/// A malformed tree uses the original walk: a commit found before a bad
80−/// sibling must still be readable, and duplicate ids keep their first match.
81−pub fn commit_by_id(repo: &Value, id: &str) -> Result<Option<Value>, Crash> {
82− if let Value::Record(key) = repo {
83− let indexed = COMMIT_INDEX.with(|cache| {
84− let mut cache = cache.borrow_mut();
85− if !cache.as_ref().is_some_and(|(k, _)| std::rc::Rc::ptr_eq(k, key)) {
86− let index = (|| -> Result<HashMap<String, Value>, Crash> {
87− let mut index = HashMap::new();
88− let mut pending = vec![top_of(repo)?];
89− while let Some(tree) = pending.pop() {
90− let commit = tree.field("root")?;
91− index.entry(id_of(&commit)?).or_insert(commit);
92− pending.extend(tree.field("children")?.as_list()?.iter().rev().cloned());
93− }
94− Ok(index)
95− })().ok();
96− *cache = Some((key.clone(), index));
97− }
98− cache.as_ref().unwrap().1.as_ref().map(|index| index.get(id).cloned())
99− });
100− if let Some(found) = indexed {
101− return Ok(found);
102− }
103− }
104− by_id(repo, id)?.map(|r| r.field("root")).transpose()
105−}
106−
107−/// compare a commit's id without copying it out
108−fn id_is(commit: &Value, id: &str) -> Result<bool, Crash> {
109− match commit.field("id")? {
110− Value::Id(i) => Ok(i.as_str() == id),
111− v => Err(Crash::new(format!("expected an Id, got a {}", v.kind_name()))),
112 }
23 }
24+ Ok(None)
113}
25}
114
26
115fn id_of(commit: &Value) -> Result<String, Crash> {
27fn id_of(commit: &Value) -> Result<String, Crash> {
@@ -119,46 +31,12 @@ fn id_of(commit: &Value) -> Result<String, Crash> {
119 }
31 }
120}
32}
121
33
122−thread_local! {
123− /// The last location climbed to the top, and the value it was climbed
124− /// from. A revset walk applies `by` to the same repo once per id, and
125− /// each climb rebuilds a location per level of depth; the answer only
126− /// depends on the value, which cannot change. Holding the `Rc` keeps the
127− /// address from being reused while it is the key.
128− static TOP_MEMO: std::cell::RefCell<Option<(std::rc::Rc<crate::value::RecordMap>, Value)>> =
129− const { std::cell::RefCell::new(None) };
130−}
131−
132/// move a location to the top of history
34/// move a location to the top of history
133fn top_of(repo: &Value) -> Result<Value, Crash> {
35fn top_of(repo: &Value) -> Result<Value, Crash> {
134− let key = match repo {
135− Value::Record(m) => Some(m.clone()),
136− _ => None,
137− };
138− if let Some(k) = &key {
139− let hit = TOP_MEMO.with(|c| {
140− c.borrow().as_ref().and_then(|(cached, top)| {
141− std::rc::Rc::ptr_eq(cached, k).then(|| top.clone())
142− })
143− });
144− if let Some(top) = hit {
145− return Ok(top);
146− }
147− }
148− let top = top_of_uncached(repo)?;
149− if let Some(k) = key {
150− TOP_MEMO.with(|c| *c.borrow_mut() = Some((k, top.clone())));
151− }
152− Ok(top)
153−}
154−
155−fn top_of_uncached(repo: &Value) -> Result<Value, Crash> {
156 let mut cur = repo.clone();
36 let mut cur = repo.clone();
157 loop {
37 loop {
158− // ask whether the context is empty without copying it: this runs once
159− // per level, and copying made climbing quadratic in the depth
160− let ctx = cur.field("context")?;
38+ let ctx = cur.field("context").and_then(|v| v.as_list().map(|x| x.to_vec()))?;
161− if ctx.as_list()?.is_empty() {
39+ if ctx.is_empty() {
162 return Ok(cur);
40 return Ok(cur);
163 }
41 }
164 cur = up_of(&cur)?;
42 cur = up_of(&cur)?;
@@ -182,18 +60,50 @@ fn up_of(repo: &Value) -> Result<Value, Crash> {
182 ("children", repo.field("children")?),
60 ("children", repo.field("children")?),
183 ]));
61 ]));
184 children.extend_from_slice(right);
62 children.extend_from_slice(right);
185− // the remaining context shares the frames rather than copying them
186− let rest = match repo.field("context")? {
187− Value::List(xs) => Value::List(xs.skip(1)),
188− v => return Err(Crash::new(format!("expected a list, got a {}", v.kind_name()))),
189− };
190 Ok(Value::record(&[
63 Ok(Value::record(&[
191 ("children", Value::list(children)),
64 ("children", Value::list(children)),
192− ("context", rest),
65+ ("context", Value::list(ctx[1..].to_vec())),
193 ("root", parent),
66 ("root", parent),
194 ]))
67 ]))
195}
68}
196
69
70+fn refocus_child(repo: &Value, child: &Value) -> Result<Value, Crash> {
71+ let children = repo.field("children")?;
72+ let children = children.as_list()?;
73+ let mut left = Vec::new();
74+ let mut right = Vec::new();
75+ let mut found = false;
76+ // match by the child's root id: ids are unique per repo (§7.5 validates
77+ // this at persist time), and a deep value_eq here would compare the whole
78+ // subtree — files included — making by_id quadratic in history size
79+ let want_id = id_of(&child.field("root")?).ok();
80+ for c in children.iter() {
81+ let same = match (&want_id, id_of(&c.field("root")?).ok()) {
82+ (Some(w), Some(cid)) => *w == cid,
83+ _ => value_eq(c, child)?,
84+ };
85+ if !found && same {
86+ found = true;
87+ } else if !found {
88+ left.push(c.clone());
89+ } else {
90+ right.push(c.clone());
91+ }
92+ }
93+ let frame = Value::record(&[
94+ ("left", Value::list(left)),
95+ ("parent", repo.field("root")?),
96+ ("right", Value::list(right)),
97+ ]);
98+ let mut ctx = repo.field("context")?.as_list()?.to_vec();
99+ ctx.insert(0, frame);
100+ Ok(Value::record(&[
101+ ("children", child.field("children")?),
102+ ("context", Value::list(ctx)),
103+ ("root", child.field("root")?),
104+ ]))
105+}
106+
197// ----------------------------------------------------------------------
107// ----------------------------------------------------------------------
198// tree traversal helpers over Repo values
108// tree traversal helpers over Repo values
199// ----------------------------------------------------------------------
109// ----------------------------------------------------------------------
@@ -224,166 +134,18 @@ pub fn parent_map(repo: &Value) -> Result<Vec<(String, String)>, Crash> {
224 Ok(out)
134 Ok(out)
225}
135}
226
136
227−/// `tree` is the top location or a subtree below it: only its `root` and
228−/// `children` are read. Refocusing on each child instead copied the context
229−/// at every level, which is quadratic in the depth of the history, in time
230−/// and in what the walk holds at once.
231−fn parent_map_rec(tree: &Value, out: &mut Vec<(String, String)>) -> Result<(), Crash> {
137+fn parent_map_rec(loc: &Value, out: &mut Vec<(String, String)>) -> Result<(), Crash> {
232− let pid = id_of(&tree.field("root")?)?;
138+ let pid = id_of(&loc.field("root")?)?;
233− let children = tree.field("children")?;
139+ let children = loc.field("children")?;
234 for c in children.as_list()?.iter() {
140 for c in children.as_list()?.iter() {
235 out.push((id_of(&c.field("root")?)?, pid.clone()));
141 out.push((id_of(&c.field("root")?)?, pid.clone()));
236− parent_map_rec(c, out)?;
142+ parent_map_rec(&refocus_child(loc, c)?, out)?;
237 }
143 }
238 Ok(())
144 Ok(())
239}
145}
240
146
241// ----------------------------------------------------------------------
147// ----------------------------------------------------------------------
242−// equality up to the order of snapshot entries (§7.3)
243−// ----------------------------------------------------------------------
244−
245−/// Whether two snapshots hold the same entries, in whatever order. A
246−/// snapshot stands for a tree, which has no order (§7.3): `select` lists the
247−/// entries a fileset matches first, so a `contract m` that moves nothing
248−/// still reorders the parent's files, and comparing them as lists called
249−/// that a change. Entries compare with `value_eq`, so lazy blobs still
250−/// compare by content id without being read, and two lists loaded from the
251−/// same stored tree (`stored_tree`) are the same without either being read.
252−pub fn snapshot_eq(a: &Value, b: &Value) -> Result<bool, Crash> {
253− if let (Some(x), Some(y)) = (stored_tree(a), stored_tree(b)) {
254− if x == y {
255− return Ok(true);
256− }
257− }
258− let (a, b) = (a.forced()?, b.forced()?);
259− // not lists: no snapshots, left to validation to name (§7.5 step 1)
260− let (Value::List(xs), Value::List(ys)) = (&a, &b) else {
261− return value_eq(&a, &b);
262− };
263− if xs.len() != ys.len() {
264− return Ok(false);
265− }
266− // the usual case, both in path order: one pass, nothing allocated
267− let mut k = 0;
268− while k < xs.len() && value_eq(&xs[k], &ys[k])? {
269− k += 1;
270− }
271− if k == xs.len() {
272− return Ok(true);
273− }
274− // a tree has one listing in path order, the one it is loaded in and
275− // `replay` returns, so two such listings that differ are different trees
276− if in_path_order(xs) && in_path_order(ys) {
277− return Ok(false);
278− }
279− // the rest by path; an entry with no readable path, or a path listed
280− // twice, is not a tree, and compares unequal as it would as a list
281− let mut rest: BTreeMap<Vec<String>, &Value> = BTreeMap::new();
282− for x in &xs[k..] {
283− let Some(p) = entry_path(x) else {
284− return Ok(false);
285− };
286− if rest.insert(p, x).is_some() {
287− return Ok(false);
288− }
289− }
290− for y in &ys[k..] {
291− match entry_path(y).and_then(|p| rest.remove(&p)) {
292− Some(x) if value_eq(x, y)? => {}
293− _ => return Ok(false),
294− }
295− }
296− Ok(true)
297−}
298−
299−/// A commit's `files` as it holds them: a lazy list is not read.
300−pub fn files_of(commit: &Value) -> Result<Value, Crash> {
301− if let Value::Record(m) = commit {
302− if let Some(files) = m.get("files") {
303− return Ok(files.clone());
304− }
305− }
306− commit.field("files")
307−}
308−
309−/// The name of the stored tree `files` loads, read or not, while they are
310−/// the list the backend built from it (`ThunkVal::tree`); `None` for a list
311−/// the program built, even one with the same entries.
312−pub fn stored_tree(files: &Value) -> Option<&str> {
313− match files {
314− Value::Thunk(t) => t.tree(),
315− _ => None,
316− }
317−}
318−
319−fn entry_path(entry: &Value) -> Option<Vec<String>> {
320− let path = entry.field("path").ok()?;
321− let comps = path.as_list().ok()?;
322− comps.iter().map(|c| c.as_text().ok().map(str::to_string)).collect()
323−}
324−
325−/// Every entry's path strictly after the one before, component by component
326−/// (`a/b` < `a.txt`); false if a path is unreadable.
327−fn in_path_order(entries: &[Value]) -> bool {
328− let before = |a: &Value, b: &Value| -> Option<bool> {
329− let (pa, pb) = (a.field("path").ok()?, b.field("path").ok()?);
330− let (ca, cb) = (pa.as_list().ok()?, pb.as_list().ok()?);
331− for (x, y) in ca.iter().zip(cb.iter()) {
332− match x.as_text().ok()?.cmp(y.as_text().ok()?) {
333− std::cmp::Ordering::Equal => {}
334− o => return Some(o.is_lt()),
335− }
336− }
337− Some(ca.len() < cb.len())
338− };
339− entries.windows(2).all(|w| before(&w[0], &w[1]) == Some(true))
340−}
341−
342−/// Whether `b` is the repository `a` (§1.2 step 8): equal as values, except
343−/// that each commit's `files` compare as snapshots (`snapshot_eq`), so a
344−/// result that only reorders entries persists nothing. A record `b` shares
345−/// with `a` is equal without being walked.
346−pub fn same_repo(a: &Value, b: &Value) -> Result<bool, Crash> {
347− match (a, b) {
348− (Value::Record(x), Value::Record(y)) => {
349− if std::rc::Rc::ptr_eq(x, y) {
350− return Ok(true);
351− }
352− if x.len() != y.len() {
353− return Ok(false);
354− }
355− for ((k1, v1), (k2, v2)) in x.iter().zip(y.iter()) {
356− // only a Commit has a `files` field in a Repo value
357− let same = k1 == k2
358− && if k1 == "files" {
359− snapshot_eq(v1, v2)?
360− } else {
361− same_repo(v1, v2)?
362− };
363− if !same {
364− return Ok(false);
365− }
366− }
367− Ok(true)
368− }
369− (Value::List(xs), Value::List(ys)) => {
370− if xs.len() != ys.len() {
371− return Ok(false);
372− }
373− for (u, v) in xs.iter().zip(ys.iter()) {
374− if !same_repo(u, v)? {
375− return Ok(false);
376− }
377− }
378− Ok(true)
379− }
380− _ => value_eq(a, b),
381− }
382−}
383−
384−// ----------------------------------------------------------------------
385−// validation (§7.5 steps 1–3, the root invariant of 4, and 6), shared by
148+// validation (§7.5 steps 1–3 and 6), shared by `validate` and persistence
386−// `validate` and persistence
387// ----------------------------------------------------------------------
149// ----------------------------------------------------------------------
388
150
389#[derive(Debug)]
151#[derive(Debug)]
@@ -409,8 +171,6 @@ pub fn validate_repo(i: &mut Interp, new: &Value) -> Result<Validated, Crash> {
409 if let Some(old) = &old {
171 if let Some(old) = &old {
410 validate_immutable(old, new, &immutable)?;
172 validate_immutable(old, new, &immutable)?;
411 }
173 }
412− let given = i.given_repo.borrow().clone().or_else(|| old.clone());
413− validate_path_names(old.as_ref(), given.as_ref(), new, &immutable, i.backend.as_ref())?;
414 // focus mutable (§7.5 step 6)
174 // focus mutable (§7.5 step 6)
415 let focus_id = id_of(&new.field("root")?)?;
175 let focus_id = id_of(&new.field("root")?)?;
416 if focus_id == ROOT_ID || immutable.contains(&focus_id) {
176 if focus_id == ROOT_ID || immutable.contains(&focus_id) {
@@ -418,29 +178,9 @@ pub fn validate_repo(i: &mut Interp, new: &Value) -> Result<Validated, Crash> {
418 "the focus must be a mutable commit; compose with new",
178 "the focus must be a mutable commit; compose with new",
419 ));
179 ));
420 }
180 }
421− validate_root(old.as_ref(), new)?;
422 Ok(Validated { immutable })
181 Ok(Validated { immutable })
423}
182}
424
183
425−/// The root is fixed even when the configured immutable set is empty.
426−/// Compare with the loaded root, preserving synthetic backends' contents
427−/// and the lazy files of a root that has not changed.
428−fn validate_root(old: Option<&Value>, new: &Value) -> Result<(), Crash> {
429− let root = top_of(new)?.field("root")?;
430− if id_of(&root)? != ROOT_ID {
431− return Err(Crash::new("persistence: the root commit must be the top"));
432− }
433− if let Some(old) = old {
434− let stored = top_of(old)?.field("root")?;
435− if !value_eq(&stored.field("message")?, &root.field("message")?)?
436− || !same_files(&stored, &root)?
437− {
438− return Err(Crash::new("persistence: the root commit cannot be changed"));
439− }
440− }
441− Ok(())
442−}
443−
444fn check_repo_shape(v: &Value) -> Result<(), Crash> {
184fn check_repo_shape(v: &Value) -> Result<(), Crash> {
445 let fields = v
185 let fields = v
446 .field_set()
186 .field_set()
@@ -471,9 +211,8 @@ fn validate_ids_and_snapshots(repo: &Value, seen: &mut BTreeSet<String>) -> Resu
471 validate_tree(&top, seen)
211 validate_tree(&top, seen)
472}
212}
473
213
474−/// `tree` is the top location or a subtree below it, as in `parent_map_rec`
475−fn validate_tree(tree: &Value, seen: &mut BTreeSet<String>) -> Result<(), Crash> {
214+fn validate_tree(loc: &Value, seen: &mut BTreeSet<String>) -> Result<(), Crash> {
476− let root = tree.field("root")?;
215+ let root = loc.field("root")?;
477 let id = id_of(&root)?;
216 let id = id_of(&root)?;
478 if !seen.insert(id.clone()) {
217 if !seen.insert(id.clone()) {
479 return Err(Crash::new(format!(
218 return Err(Crash::new(format!(
@@ -481,200 +220,27 @@ fn validate_tree(tree: &Value, seen: &mut BTreeSet<String>) -> Result<(), Crash>
481 id
220 id
482 )));
221 )));
483 }
222 }
484− // Persistence reads every description as Text, even when its files
485− // stay backed by a stored tree. A dry run must reject the same value.
486− root.field("message")?.as_text()?;
487− // a list loaded from a stored tree is a snapshot already: jj's tree
488− // holds each name once, and lists a path as a file or as a directory.
489− // Reading every commit's to check it made each persist read the files
490− // of the whole history
491− let files = files_of(&root)?;
223+ let files = root.field("files")?;
492− if stored_tree(&files).is_none() {
493− validate_snapshot(&files.forced()?)?;
494− }
495− let children = tree.field("children")?;
496− for c in children.as_list()?.iter() {
497− validate_tree(c, seen)?;
498− }
499− Ok(())
500−}
501−
502−/// Entries have Blob contents and unique paths, none of them the root `./`
503−/// or both a file and a directory (§7.3, §7.5 step 1).
504−fn validate_snapshot(files: &Value) -> Result<(), Crash> {
505 let files = files.as_list()?;
224 let files = files.as_list()?;
506 let mut paths = BTreeSet::new();
225 let mut paths = BTreeSet::new();
507 for e in files {
226 for e in files {
508 let p = e.field("path")?;
227 let p = e.field("path")?;
509− // Inspect the value's kind only: binary and unresolved blobs are
510− // valid, and a lazy blob need not read its bytes to pass validation.
511− if !matches!(e.field("content")?, Value::Blob(_)) {
512− return Err(Crash::new("persistence: snapshot content is not a Blob"));
513− }
514 let p = p.as_list()?;
228 let p = p.as_list()?;
515 let key: Vec<String> = p
229 let key: Vec<String> = p
516 .iter()
230 .iter()
517 .map(|c| c.as_text().map(|s| s.to_string()))
231 .map(|c| c.as_text().map(|s| s.to_string()))
518 .collect::<Result<_, _>>()?;
232 .collect::<Result<_, _>>()?;
519− if key.is_empty() {
520− return Err(Crash::new("persistence: a snapshot has an entry at the root path ./"));
521− }
522 if !paths.insert(key) {
233 if !paths.insert(key) {
523 return Err(Crash::new("persistence: a snapshot has duplicate paths"));
234 return Err(Crash::new("persistence: a snapshot has duplicate paths"));
524 }
235 }
525 }
236 }
526− if let Some(p) = crate::domain::file_and_directory(&paths) {
237+ let children = loc.field("children")?;
527− return Err(Crash::new(format!(
238+ for c in children.as_list()?.iter() {
528− "persistence: a snapshot has `{}` as both a file and a directory",
239+ validate_tree(&refocus_child(loc, c)?, seen)?;
529− p.join("/")
530− )));
531− }
532− Ok(())
533−}
534−
535−/// The name limit of ext4, xfs, btrfs and tmpfs (NAME_MAX), in bytes.
536−/// exFAT, NTFS, HFS+ and APFS count 255 UTF-16 units or characters instead,
537−/// so they hold longer names, and pathconf answers 255 on each: whether a
538−/// longer name fits is asked of the backend (`Backend::name_fits`). A
539−/// filesystem with a lower limit (encfs, eCryptfs) refuses a shorter name
540−/// at checkout, which then records nothing (§7.5 step 7).
541−pub const NAME_MAX: usize = 255;
542−
543−/// Whether a checkout cannot create the path component `comp` (§7.5 step
544−/// 1). jj stores any name that is not empty and has no `/`, but its
545−/// checkout refuses `.`, `..`, `.git` and `.jj`, and the filesystem a name
546−/// it cannot hold, part way through writing the focus; a git tree cannot
547−/// hold a NUL. Where the filesystem folds case, `.GIT` is the file `.git`,
548−/// which the checkout refuses by its file identity or, where that does not
549−/// show it, writes into. `fits` says whether a name longer than `NAME_MAX`
550−/// bytes is one the filesystem holds.
551−pub fn checkout_refuses(comp: &str, folds_case: bool, fits: impl FnOnce(&str) -> bool) -> bool {
552− matches!(comp, "" | "." | ".." | ".git" | ".jj")
553− || comp.contains(['/', '\0'])
554− || (folds_case && (comp.eq_ignore_ascii_case(".git") || comp.eq_ignore_ascii_case(".jj")))
555− || (comp.len() > NAME_MAX && !fits(comp))
556−}
557−
558−/// Path components (§7.5 step 1) of every commit persisting writes anew or
559−/// checks out must be ones a checkout can create (`checkout_refuses`).
560−///
561−/// A commit that keeps the files it has in `old` is checked only as the
562−/// focus: persisting at most rewrites it with the tree jj already stored and
563−/// never checks it out, and a fetched branch can hold `.jj`, so refusing its
564−/// names would stop every persist for a commit the script did not touch.
565−/// Immutable commits are such commits (step 3). One whose files differ,
566−/// such as a descendant a snapshot of the focus rebases, is checked only at
567−/// the paths it did not hold there: jj stored the others' names already.
568−///
569−/// A name longer than `NAME_MAX` is one the filesystem holds if the
570−/// working directory holds it, as the focus of `given` (its snapshot, §7.4)
571−/// shows: the checkout, which diffs from what is on disk, does not create
572−/// such a file again. Any other is asked of the backend.
573−fn validate_path_names(
574− old: Option<&Value>,
575− given: Option<&Value>,
576− new: &Value,
577− immutable: &BTreeSet<String>,
578− backend: &dyn Backend,
579−) -> Result<(), Crash> {
580− let folds_case = backend.folds_case();
581− // the long names the working directory holds, read on first need: the
582− // snapshot's file list is the whole tree
583− let mut held: Option<BTreeSet<String>> = None;
584− let focus = id_of(&new.field("root")?)?;
585− let stored: BTreeMap<String, Value> = match old {
586− Some(old) => all_commits(old)?
587− .into_iter()
588− .map(|c| id_of(&c).map(|id| (id, c)))
589− .collect::<Result<_, _>>()?,
590− None => BTreeMap::new(),
591− };
592− for c in all_commits(new)? {
593− let id = id_of(&c)?;
594− if immutable.contains(&id) {
595− continue;
596− }
597− // the commit as stored, when its files differ: a path it held there
598− // is not checked again
599− let mut kept = None;
600− if id != focus {
601− if let Some(o) = stored.get(&id) {
602− if same_files(o, &c)? {
603− continue;
604− }
605− kept = Some(o);
606− }
607− }
608− // the paths `kept` holds, read on first need: most commits hold no
609− // name a checkout refuses
610− let mut kept_paths: Option<BTreeSet<Vec<String>>> = None;
611− let files = c.field("files")?;
612− for e in files.as_list()? {
613− let path = e.field("path")?;
614− for comp in path.as_list()? {
615− let comp = comp.as_text()?;
616− if comp.len() > NAME_MAX && held.is_none() {
617− held = Some(match given {
618− Some(given) => long_names(&given.field("root")?.field("files")?)?,
619− None => BTreeSet::new(),
620− });
621− }
622− let on_disk = |c: &str| held.as_ref().is_some_and(|h| h.contains(c));
623− if !checkout_refuses(comp, folds_case, |c| on_disk(c) || backend.name_fits(c)) {
624− continue;
625− }
626− if let Some(o) = kept {
627− if kept_paths.is_none() {
628− let entries = o.field("files")?;
629− kept_paths = Some(entries.as_list()?.iter().filter_map(entry_path).collect());
630− }
631− if let (Some(k), Some(p)) = (&kept_paths, entry_path(e)) {
632− if k.contains(&p) {
633− break;
634− }
635− }
636− }
637− return Err(Crash::new(format!(
638− "persistence: a snapshot has the path component {:?}, which a checkout cannot create",
639− comp
640− )));
641− }
642− }
643 }
240 }
644 Ok(())
241 Ok(())
645}
242}
646
243
647−/// The path components of snapshot `files` longer than `NAME_MAX` bytes.
648−fn long_names(files: &Value) -> Result<BTreeSet<String>, Crash> {
649− let mut out = BTreeSet::new();
650− for e in files.as_list()? {
651− for comp in e.field("path")?.as_list()? {
652− let comp = comp.as_text()?;
653− if comp.len() > NAME_MAX {
654− out.insert(comp.to_string());
655− }
656− }
657− }
658− Ok(out)
659−}
660−
661−/// Whether commit `new` holds the files commit `old` does. A commit record,
662−/// or a lazy `files`, the two share, or lazy lists of one stored tree, are
663−/// the same without the tree being read.
664−fn same_files(old: &Value, new: &Value) -> Result<bool, Crash> {
665− if let (Value::Record(a), Value::Record(b)) = (old, new) {
666− if std::rc::Rc::ptr_eq(a, b) {
667− return Ok(true);
668− }
669− if let (Some(Value::Thunk(x)), Some(Value::Thunk(y))) = (a.get("files"), b.get("files")) {
670− if std::rc::Rc::ptr_eq(x, y) {
671− return Ok(true);
672− }
673− }
674− }
675− snapshot_eq(&files_of(old)?, &files_of(new)?)
676−}
677−
678fn label_set(repo: &Value) -> Result<BTreeSet<(String, String)>, Crash> {
244fn label_set(repo: &Value) -> Result<BTreeSet<(String, String)>, Crash> {
679 let mut out = BTreeSet::new();
245 let mut out = BTreeSet::new();
680 for c in all_commits(repo)? {
246 for c in all_commits(repo)? {
@@ -749,7 +315,7 @@ fn validate_immutable(old: &Value, new: &Value, immutable: &BTreeSet<String>) ->
749 id
315 id
750 )));
316 )));
751 }
317 }
752− if !snapshot_eq(&files_of(&c)?, &files_of(nc)?)? {
318+ if !value_eq(&c.field("files")?, &nc.field("files")?)? {
753 return Err(Crash::new(format!(
319 return Err(Crash::new(format!(
754 "persistence: commit {} is immutable (files changed)",
320 "persistence: commit {} is immutable (files changed)",
755 id
321 id
src/setup.rsdeleted−119
@@ -1,119 +0,0 @@
1−//! Explicit first-run configuration, independent of repository discovery.
2−
3−use j::ast::Item;
4−use j::config;
5−use j::eval::Interp;
6−use j::lex::{lex, Tok};
7−use j::parse::parse_config;
8−use j::show::show;
9−use j::value::Value;
10−use std::io::Write;
11−use std::path::Path;
12−use std::rc::Rc;
13−
14−const IDENTITY_EXAMPLE: &str = "user = { name = \"Ada Lovelace\", email = \"ada@example.org\" }";
15−
16−/// `args` contains only the arguments after `setup`. Existing files, including
17−/// invalid configs and symlinks, are never replaced. A new file is published
18−/// atomically so concurrent setup commands cannot overwrite one another.
19−pub fn run(path: &Path, default: &str, args: &[&str]) -> Result<String, (u8, String)> {
20− if !matches!(args.len(), 0 | 2) {
21− return Err((2, "usage: j setup [NAME EMAIL]; for example: j setup 'Ada Lovelace' ada@example.org; see `j help setup`".into()));
22− }
23− if path.symlink_metadata().is_ok() {
24− return existing(path, args);
25− }
26− let source = match args {
27− [name, email] => with_identity(default, name, email)?,
28− [] => default.to_string(),
29− _ => unreachable!(),
30− };
31− let parent = path.parent().filter(|p| !p.as_os_str().is_empty()).unwrap_or_else(|| Path::new("."));
32− std::fs::create_dir_all(parent)
33− .map_err(|error| (3, format!("cannot create config directory {}: {}", parent.display(), error)))?;
34− let mut temporary = tempfile::NamedTempFile::new_in(parent)
35− .map_err(|error| (3, format!("cannot prepare config at {}: {}", path.display(), error)))?;
36− temporary.write_all(source.as_bytes())
37− .map_err(|error| (3, format!("cannot write config at {}: {}", path.display(), error)))?;
38− match temporary.persist_noclobber(path) {
39− Ok(_) => {}
40− Err(error) if error.error.kind() == std::io::ErrorKind::AlreadyExists => return existing(path, args),
41− Err(error) => return Err((3, format!("cannot create config at {}: {}", path.display(), error.error))),
42− }
43− if args.is_empty() {
44− Ok(format!(
45− "Created the complete default config at {}.\n\nEdit the existing `user` definition in this file, replacing both example values with your identity:\n {}\nKeep the rest of the file: it defines your commands.\n\nThen run `j init` in your project directory, or `j clone URL`.\n",
46− path.display(), IDENTITY_EXAMPLE,
47− ))
48− } else {
49− Ok(format!(
50− "Configuration ready at {}.\nIdentity: {}\n\nNext: run `j init` in your project directory, or `j clone URL`.\n",
51− path.display(), identity_line(&source)?,
52− ))
53− }
54−}
55−
56−fn existing(path: &Path, args: &[&str]) -> Result<String, (u8, String)> {
57− if !args.is_empty() {
58− return Err((2, format!(
59− "config already exists at {}; it was not changed. Edit its existing `user` definition to change your identity; see `j help config`.",
60− path.display(),
61− )));
62− }
63− let problem = match std::fs::read_to_string(path) {
64− Ok(source) => match identity_line(&source) {
65− Ok(identity) => return Ok(format!(
66− "Configuration already exists at {} and is ready.\nIdentity: {}\n\nNext: run `j init` in your project directory, or `j clone URL`.\n",
67− path.display(), identity,
68− )),
69− Err((_, message)) => message,
70− },
71− Err(error) => format!("cannot read this config: {}", error),
72− };
73− Ok(format!(
74− "Configuration already exists at {}; it was not changed.\nNeeds attention: {}\n\nTo set your identity, edit the existing `user` definition:\n {}\nKeep the rest of the file and repair the problem above.\nSee `j help config`; then retry `j setup`.\n",
75− path.display(), problem, IDENTITY_EXAMPLE,
76− ))
77−}
78−
79−fn identity_line(source: &str) -> Result<String, (u8, String)> {
80− let cfg = config::load_config(source).map_err(|error| (3, error.message()))?;
81− j::jj::configured_identity(&cfg)
82−}
83−
84−fn with_identity(default: &str, name: &str, email: &str) -> Result<String, (u8, String)> {
85− let items = parse_config(default, Rc::new(config::reserved_set()))
86− .map_err(|error| (3, format!("cannot prepare default config: {}", error.msg)))?;
87− let line = items.iter().find_map(|item| match item {
88− Item::Definition(name, _, line) if name == "user" => Some(*line),
89− _ => None,
90− }).ok_or_else(|| (3, "default config has no `user` definition".into()))?;
91− let tokens = lex(default).map_err(|error| (3, error.msg))?;
92− let start = tokens.iter().position(|token| token.line == line && token.col == 1)
93− .ok_or_else(|| (3, "cannot locate the default `user` definition".into()))?;
94− let mut depth = 0usize;
95− let mut end = tokens[start].end;
96− for token in &tokens[start..] {
97− if matches!(token.tok, Tok::Eof)
98− || (token.line > line && token.col == 1 && depth == 0 && !matches!(token.tok, Tok::Newline))
99− {
100− break;
101− }
102− match token.tok {
103− Tok::LParen | Tok::LBracket | Tok::LBrace => depth += 1,
104− Tok::RParen | Tok::RBracket | Tok::RBrace => depth = depth.saturating_sub(1),
105− Tok::Newline => continue,
106− _ => {}
107− }
108− end = token.end;
109− }
110− let interp = Interp::dummy();
111− let name = show(&interp, &Value::text(name)).map_err(|error| (3, error.msg))?;
112− let email = show(&interp, &Value::text(email)).map_err(|error| (3, error.msg))?;
113− let definition = format!("user = {{ name = {}, email = {} }}", name, email);
114− let mut source = default.to_string();
115− source.replace_range(tokens[start].start..end, &definition);
116− // Reuse the same identity requirements as init/clone before writing a file.
117− identity_line(&source)?;
118− Ok(source)
119−}
src/shape.rs+60−104
@@ -2,38 +2,25 @@
2
2
3use crate::ast::TypeExpr;
3use crate::ast::TypeExpr;
4use crate::value::{PrimKind, ShapeKind, ShapeVal, Value};
4use crate::value::{PrimKind, ShapeKind, ShapeVal, Value};
5−use std::collections::{BTreeSet, HashMap, HashSet};
5+use std::collections::{BTreeSet, HashMap};
6
6
7#[derive(Default, Clone)]
7#[derive(Default, Clone)]
8pub struct Shapes {
8pub struct Shapes {
9 /// name -> its declared type
9 /// name -> its declared type
10 pub decls: HashMap<String, TypeExpr>,
10 pub decls: HashMap<String, TypeExpr>,
11− /// Memo for `shape_of`. A contract check against a record type asks for
12− /// the shape on every call, and computing it builds the field set afresh;
13− /// the declarations never change once the config is loaded.
14− memo: std::cell::RefCell<HashMap<String, Option<ShapeVal>>>,
15}
11}
16
12
17impl Shapes {
13impl Shapes {
18 pub fn new() -> Shapes {
14 pub fn new() -> Shapes {
19− Shapes::default()
15+ Shapes {
16+ decls: HashMap::new(),
17+ }
20 }
18 }
21
19
22 /// Compute the ShapeVal for a type name used as a value (§4.12).
20 /// Compute the ShapeVal for a type name used as a value (§4.12).
23 /// Returns None if the name is undeclared, or its shape is a function,
21 /// Returns None if the name is undeclared, or its shape is a function,
24 /// a list, or otherwise not usable.
22 /// a list, or otherwise not usable.
25 pub fn shape_of(&self, name: &str) -> Option<ShapeVal> {
23 pub fn shape_of(&self, name: &str) -> Option<ShapeVal> {
26− if let Some(hit) = self.memo.borrow().get(name) {
27− return hit.clone();
28− }
29− let computed = self.shape_of_uncached(name);
30− self.memo
31− .borrow_mut()
32− .insert(name.to_string(), computed.clone());
33− computed
34− }
35−
36− fn shape_of_uncached(&self, name: &str) -> Option<ShapeVal> {
37 match name {
24 match name {
38 "Int" => Some(ShapeVal {
25 "Int" => Some(ShapeVal {
39 name: name.into(),
26 name: name.into(),
@@ -67,13 +54,18 @@ impl Shapes {
67 }
54 }
68
55
69 fn shape_of_type(&self, ty: &TypeExpr) -> Option<ShapeKind> {
56 fn shape_of_type(&self, ty: &TypeExpr) -> Option<ShapeKind> {
70− // an alias takes its type's shape (§4.12); `unfold` is bounded, so a
71− // cycle of aliases (`Self = Self`) ends on a name like any other
72− match self.unfold(ty) {
57+ match ty {
58+ TypeExpr::Con(n) => {
73− TypeExpr::Con(n) if is_primitive(n) => self.shape_of(n).map(|s| s.kind),
59+ if is_primitive(n) {
74− // undeclared, a type variable, or an alias cycle: not a usable
60+ let s = self.shape_of(n)?;
75− // shape, and a crash when used (§4.12)
61+ Some(s.kind)
76− TypeExpr::Con(_) => None,
62+ } else if let Some(decl) = self.decls.get(n) {
63+ // resolve the alias now that all decls are present
64+ self.shape_of_type(decl)
65+ } else {
66+ Some(ShapeKind::Aliased(n.clone()))
67+ }
68+ }
77 TypeExpr::Record(fields) => Some(ShapeKind::Record(
69 TypeExpr::Record(fields) => Some(ShapeKind::Record(
78 fields.iter().map(|(n, _)| n.clone()).collect(),
70 fields.iter().map(|(n, _)| n.clone()).collect(),
79 )),
71 )),
@@ -81,25 +73,21 @@ impl Shapes {
81 }
73 }
82 }
74 }
83
75
84− /// Unfold an alias chain by reference. Contracts are checked at every
76+ /// Unfold an alias chain, owned, so it works with partially-populated
85− /// call, so cloning the type each time — a `Vec` of `String`s for a record
77+ /// decl maps too.
86− /// type — was a per-application allocation.
87− pub fn unfold<'a>(&'a self, ty: &'a TypeExpr) -> &'a TypeExpr {
78+ pub fn unfold_owned(&self, ty: &TypeExpr) -> TypeExpr {
88− let mut cur = ty;
79+ match ty {
89− // a declaration cycle would otherwise spin here; the bound is far
80+ TypeExpr::Con(n) => {
90− // above any real alias chain
91− for _ in 0..64 {
92− match cur {
93− TypeExpr::Con(n) if !is_primitive(n) && !is_type_var(n) => {
81+ if is_primitive(n) || is_type_var(n) {
82+ return ty.clone();
83+ }
94− match self.decls.get(n) {
84+ match self.decls.get(n) {
95− Some(decl) => cur = decl,
85+ Some(decl) => self.unfold_owned(decl),
96− None => return cur,
86+ None => ty.clone(),
97− }
98 }
87 }
99− _ => return cur,
100 }
88 }
89+ _ => ty.clone(),
101 }
90 }
102− cur
103 }
91 }
104}
92}
105
93
@@ -112,30 +100,24 @@ fn is_type_var(n: &str) -> bool {
112}
100}
113
101
114/// A compiled signature: a chain of parameter types and a final result type.
102/// A compiled signature: a chain of parameter types and a final result type.
115−/// The types are shared, so handing one out is a refcount bump rather than a
116−/// deep copy — contracts are consulted on every call.
117#[derive(Clone, Debug)]
103#[derive(Clone, Debug)]
118pub struct Contract {
104pub struct Contract {
119− pub params: Vec<Rc2<TypeExpr>>,
105+ pub params: Vec<TypeExpr>,
120− pub result: Rc2<TypeExpr>,
106+ pub result: TypeExpr,
121}
107}
122
108
123pub fn compile_contract(shapes: &Shapes, ty: &TypeExpr) -> Contract {
109pub fn compile_contract(shapes: &Shapes, ty: &TypeExpr) -> Contract {
124 let mut params = Vec::new();
110 let mut params = Vec::new();
125 let mut cur = ty;
111 let mut cur = ty;
126− // an alias met again on this walk refers to itself (`Stream = Int ->
127− // Stream`, or `A = B` with `B = A`) and would unfold forever; the walk
128− // stops there, and the result is checked as that alias
129− let mut unfolded: HashSet<&str> = HashSet::new();
130 loop {
112 loop {
131 match cur {
113 match cur {
132 TypeExpr::Fun(a, b) => {
114 TypeExpr::Fun(a, b) => {
133− params.push(a.clone());
115+ params.push((**a).clone());
134 cur = b;
116 cur = b;
135 }
117 }
136 TypeExpr::Con(n) => {
118 TypeExpr::Con(n) => {
137 // alias of a function type is unfolded (§4.13)
119 // alias of a function type is unfolded (§4.13)
138− if !is_primitive(n) && !is_type_var(n) && unfolded.insert(n.as_str()) {
120+ if !is_primitive(n) && !is_type_var(n) {
139 if let Some(decl) = shapes.decls.get(n) {
121 if let Some(decl) = shapes.decls.get(n) {
140 cur = decl;
122 cur = decl;
141 continue;
123 continue;
@@ -148,13 +130,14 @@ pub fn compile_contract(shapes: &Shapes, ty: &TypeExpr) -> Contract {
148 }
130 }
149 Contract {
131 Contract {
150 params,
132 params,
151− result: Rc2::new(cur.clone()),
133+ result: cur.clone(),
152 }
134 }
153}
135}
154
136
155/// Check one level deep (§4.13). On failure, describe expected and got.
137/// Check one level deep (§4.13). On failure, describe expected and got.
156pub fn check(shapes: &Shapes, ty: &TypeExpr, v: &Value) -> Result<(), String> {
138pub fn check(shapes: &Shapes, ty: &TypeExpr, v: &Value) -> Result<(), String> {
157− let ty = shapes.unfold(ty);
139+ let owned = shapes.unfold_owned(ty);
140+ let ty = &owned;
158 match ty {
141 match ty {
159 TypeExpr::Con(n) => {
142 TypeExpr::Con(n) => {
160 if is_type_var(n) {
143 if is_type_var(n) {
@@ -259,10 +242,7 @@ use std::rc::Rc as Rc2;
259pub enum ContractExpr {
242pub enum ContractExpr {
260 /// no contract information; arg checks pass through
243 /// no contract information; arg checks pass through
261 Unknown,
244 Unknown,
262− /// a signature with its first `at` parameters already supplied; advancing
263− /// it is an index bump, where rebuilding the shortened parameter list
264− /// copied every remaining type on every application
265− Known { contract: Rc2<Contract>, at: usize },
245+ Known(Rc2<Contract>),
266 /// compose(f, g): (f . g); both contract expressions
246 /// compose(f, g): (f . g); both contract expressions
267 Compose(Box<ContractExpr>, Box<ContractExpr>),
247 Compose(Box<ContractExpr>, Box<ContractExpr>),
268 /// apply e to an argument that checked against e's first parameter
248 /// apply e to an argument that checked against e's first parameter
@@ -271,14 +251,14 @@ pub enum ContractExpr {
271
251
272impl ContractExpr {
252impl ContractExpr {
273 /// The type of the next parameter this function expects, if known.
253 /// The type of the next parameter this function expects, if known.
274− pub fn next_param(&self) -> Option<Rc2<TypeExpr>> {
254+ pub fn next_param(&self) -> Option<TypeExpr> {
275 match self {
255 match self {
276 ContractExpr::Unknown => None,
256 ContractExpr::Unknown => None,
277− ContractExpr::Known { contract, at } => contract.params.get(*at).cloned(),
257+ ContractExpr::Known(c) => c.params.first().cloned(),
278− // (f . g) x = f (g x) (§4.9): the argument is g's, even when
279− // g's contract is unknown or used up, and f's own contract
280− // checks what g returns when it is applied to it
281− ContractExpr::Compose(_, g) => g.next_param(),
258+ ContractExpr::Compose(f, g) => g
259+ .next_param()
260+ .or_else(|| f.next_param())
261+ .or_else(|| f.result_after()),
282 ContractExpr::ApplyFirst(e) => {
262 ContractExpr::ApplyFirst(e) => {
283 let advanced = ContractExpr::apply_first(e.clone());
263 let advanced = ContractExpr::apply_first(e.clone());
284 advanced.next_param()
264 advanced.next_param()
@@ -288,12 +268,12 @@ impl ContractExpr {
288 }
268 }
289
269
290 /// The result type once all parameters are supplied.
270 /// The result type once all parameters are supplied.
291− pub fn result_after(&self) -> Option<Rc2<TypeExpr>> {
271+ pub fn result_after(&self) -> Option<TypeExpr> {
292 match self {
272 match self {
293 ContractExpr::Unknown => None,
273 ContractExpr::Unknown => None,
294− ContractExpr::Known { contract, at } => {
274+ ContractExpr::Known(c) => {
295− if *at >= contract.params.len() {
275+ if c.params.is_empty() {
296− Some(contract.result.clone())
276+ Some(c.result.clone())
297 } else {
277 } else {
298 None
278 None
299 }
279 }
@@ -313,7 +293,7 @@ impl ContractExpr {
313 pub fn is_exhausted(&self) -> bool {
293 pub fn is_exhausted(&self) -> bool {
314 match self {
294 match self {
315 ContractExpr::Unknown => false,
295 ContractExpr::Unknown => false,
316− ContractExpr::Known { contract, at } => *at >= contract.params.len(),
296+ ContractExpr::Known(c) => c.params.is_empty(),
317 ContractExpr::Compose(_, g) => g.is_exhausted(),
297 ContractExpr::Compose(_, g) => g.is_exhausted(),
318 ContractExpr::ApplyFirst(_) => false,
298 ContractExpr::ApplyFirst(_) => false,
319 }
299 }
@@ -321,24 +301,19 @@ impl ContractExpr {
321
301
322 /// Advance after one argument has been supplied and checked.
302 /// Advance after one argument has been supplied and checked.
323 pub fn apply_first_rc(self: Rc2<ContractExpr>) -> ContractExpr {
303 pub fn apply_first_rc(self: Rc2<ContractExpr>) -> ContractExpr {
324− // the common case advances without allocating anything
325− match &*self {
326− ContractExpr::Unknown => ContractExpr::Unknown,
327− ContractExpr::Known { contract, at } => ContractExpr::Known {
328− contract: contract.clone(),
329− at: contract.params.len().min(at + 1),
330− },
331− _ => ContractExpr::apply_first(Box::new((*self).clone())),
304+ ContractExpr::apply_first(Box::new((*self).clone()))
332− }
333 }
305 }
334
306
335 /// Advance after one argument has been supplied and checked.
307 /// Advance after one argument has been supplied and checked.
336 pub fn apply_first(self: Box<ContractExpr>) -> ContractExpr {
308 pub fn apply_first(self: Box<ContractExpr>) -> ContractExpr {
337 match *self {
309 match *self {
338 ContractExpr::Unknown => ContractExpr::Unknown,
310 ContractExpr::Unknown => ContractExpr::Unknown,
339− ContractExpr::Known { contract, at } => {
311+ ContractExpr::Known(c) => {
340− let at = contract.params.len().min(at + 1);
312+ let mut c2 = (*c).clone();
313+ if !c2.params.is_empty() {
314+ c2.params.remove(0);
315+ }
341− ContractExpr::Known { contract, at }
316+ ContractExpr::Known(Rc2::new(c2))
342 }
317 }
343 ContractExpr::Compose(f, g) => {
318 ContractExpr::Compose(f, g) => {
344 if !g.is_exhausted() {
319 if !g.is_exhausted() {
@@ -355,35 +330,16 @@ impl ContractExpr {
355 /// Number of remaining parameters, if known and not nested.
330 /// Number of remaining parameters, if known and not nested.
356 pub fn params_len(&self) -> usize {
331 pub fn params_len(&self) -> usize {
357 match self {
332 match self {
358− ContractExpr::Known { contract, at } => contract.params.len() - at,
333+ ContractExpr::Known(c) => c.params.len(),
359 _ => 99,
334 _ => 99,
360 }
335 }
361 }
336 }
362
337
363− /// The 0-based position of the next argument among those the signature
338+ /// True if the next parameter is definitely a function type.
364− /// lists, when known. Messages number arguments by the signature (§4.13:
339+ pub fn next_param_is_function(&self, shapes: &Shapes) -> bool {
365− /// "describe expected Text as argument 1"), not by what the function
366− /// value already holds: `tree`, which is `treeWith` applied to its
367− /// options, takes its argument 1, and so does `squash`, a composition.
368− pub fn position(&self) -> Option<usize> {
369− match self {
340+ match self.next_param() {
370− ContractExpr::Known { at, .. } => Some(*at),
341+ Some(t) => matches!(shapes.unfold_owned(&t), TypeExpr::Fun(_, _)),
371− ContractExpr::Compose(_, g) => g.position(),
372− _ => None,
373− }
374− }
375−
376− /// True when both are the same signature at the same position, so that
377− /// checking against one after the other repeats every check: the same
378− /// definition's contract, advanced as far. Only a known signature is
379− /// compared; any other is never the same.
380− pub fn same_as(&self, other: &ContractExpr) -> bool {
381− match (self, other) {
382− (
383− ContractExpr::Known { contract: a, at: i },
384− ContractExpr::Known { contract: b, at: j },
385− ) => Rc2::ptr_eq(a, b) && i == j,
386− _ => false,
342+ None => false,
387 }
343 }
388 }
344 }
389
345
src/show.rs+137−362
@@ -1,185 +1,103 @@
1//! `show` (§5.2), unified diff (§4.9 `diff`), and the difftastic bridge (§7.10).
1//! `show` (§5.2), unified diff (§4.9 `diff`), and the difftastic bridge (§7.10).
2
2
3use crate::eval::Interp;
3use crate::eval::Interp;
4−use crate::value::{BlobContent, BlobKind, BlobVal, Crash, FunVal, ThunkVal, Value};
4+use crate::value::{BlobContent, Crash, FunVal, Value};
5−use num_bigint::BigInt;
6−use unicode_width::UnicodeWidthChar;
7
5
8−/// Crashes where `v` holds a lazy part the store cannot read (§7.2), as any
9−/// other use of it does: a stand-in for the part would be the literal of
10−/// some other value, or would not parse.
11−///
12−/// Every level of `v` appends to the one string: a level that built its own
13−/// and copied its child's rendering into it cost the rendering's size times
14−/// its depth, and a Repo whose focus has a long line of descendants nests
15−/// them hundreds of levels deep, in megabytes of indentation. The renderers
16−/// recurse natively, once per level, so they also keep their frames small:
17−/// plain loops, a call's result returned as theirs without a `?` where it can
18−/// be, and the arms that end the recursion or seldom carry it on (a leaf, a
19−/// lazy part, a function) out of line.
20−pub fn show(interp: &Interp, v: &Value) -> Result<String, Crash> {
6+pub fn show(interp: &Interp, v: &Value) -> String {
21− let mut s = String::new();
22− render(interp, v, &mut s)?;
7+ let s = render(interp, v);
23− // line breaking: one line if it fits in 80 columns, counted in display
8+ // line breaking: one line if it fits in 80 columns
24− // cells as everywhere else (§5.1), not code points. Not by
9+ if s.chars().count() <= 80 {
25− // `render::width`, which skips colour codes: `show` writes none, and an
26− // ESC from the data would hide everything up to the next `m`
27− let w: usize = s.chars().map(|c| UnicodeWidthChar::width(c).unwrap_or(0)).sum();
28− if w <= 80 {
29− return Ok(s);
10+ return s;
30 }
11 }
31− s.clear();
32− render_wide(interp, v, 0, &mut s)?;
12+ render_wide(interp, v, 0)
33− Ok(s)
34}
13}
35
14
36−fn render(interp: &Interp, v: &Value, out: &mut String) -> Result<(), Crash> {
15+fn render(interp: &Interp, v: &Value) -> String {
37 match v {
16 match v {
38− Value::Thunk(t) => return render_forced(interp, t, None, out),
17+ Value::Thunk(t) => match t.force() {
18+ Ok(v) => render(interp, &v),
19+ Err(_) => "<lazy>".to_string(),
20+ },
39− Value::Int(n) => render_int(n, out),
21+ Value::Int(n) => {
40− Value::Text(t) => push_text_literal(t, out),
22+ if n.sign() == num_bigint::Sign::Minus {
41− Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
23+ format!("(0 - {})", n.magnitude())
42− Value::Id(id) => {
24+ } else {
43− out.push('@');
25+ n.to_string()
44− out.push_str(id);
26+ }
45 }
27 }
28+ Value::Text(t) => text_literal(t),
29+ Value::Bool(b) => b.to_string(),
30+ Value::Id(id) => format!("@{}", id),
46 Value::List(xs) => {
31 Value::List(xs) => {
47 // list elements must be atoms: parenthesise applications and
32 // list elements must be atoms: parenthesise applications and
48 // operator expressions (§3.4)
33 // operator expressions (§3.4)
49− out.push('[');
50− for (i, x) in xs.iter().enumerate() {
51− if i > 0 {
52− out.push(' ');
53− }
54− render_atom(interp, x, out)?;
34+ let parts: Vec<String> = xs.iter().map(|x| render_atom(interp, x)).collect();
55− }
35+ format!("[{}]", parts.join(" "))
56− out.push(']');
57 }
36 }
58 Value::Record(m) => {
37 Value::Record(m) => {
59− out.push_str("{ ");
38+ let parts: Vec<String> = m
60− for (i, (k, x)) in m.iter().enumerate() {
39+ .iter()
61− if i > 0 {
40+ .map(|(k, x)| format!("{} = {}", k, render(interp, x)))
62− out.push_str(", ");
41+ .collect();
63− }
42+ format!("{{ {} }}", parts.join(", "))
64− out.push_str(k);
65− out.push_str(" = ");
66− render(interp, x, out)?;
67− }
68− out.push_str(" }");
69− }
70− Value::Blob(b) => return render_blob(interp, b, out),
71− Value::Shape(s) => out.push_str(&s.name),
72− Value::Fun(f) => return render_fun(interp, f, out),
73− }
74− Ok(())
75−}
76−
77−#[inline(never)]
78−fn render_int(n: &BigInt, out: &mut String) {
79− if n.sign() == num_bigint::Sign::Minus {
80− out.push_str(&format!("(0 - {})", n.magnitude()));
81− } else {
82− out.push_str(&n.to_string());
83− }
84−}
85−
86−#[inline(never)]
87−fn render_blob(interp: &Interp, b: &BlobVal, out: &mut String) -> Result<(), Crash> {
88− // Unresolved blobs render as marker text, explicitly outside the
89− // round-trip guarantee. Resolved blobs must preserve bytes and kind.
90− if b.is_unresolved() {
91− let bytes = b.bytes()?;
92− out.push_str("{- unresolved -} blob ");
93− push_text_literal(&String::from_utf8_lossy(&bytes), out);
94− return Ok(());
95− }
96− let bytes = match &b.content {
97− BlobContent::Resolved(bytes) => bytes.clone(),
98− BlobContent::Lazy(lazy) => lazy.force()?,
99− BlobContent::Conflict(_) => unreachable!("handled above"),
100− };
101− if b.kind == BlobKind::Regular {
102− if let Ok(text) = std::str::from_utf8(&bytes) {
103− out.push_str("blob ");
104− push_text_literal(text, out);
105− return Ok(());
106 }
43 }
107− }
44+ Value::Blob(b) => match &b.content {
108− // Existing user configs are never upgraded automatically. Do not emit
45+ BlobContent::Resolved(bytes) => match String::from_utf8(bytes.as_ref().clone()) {
109− // an expression they cannot read back when they lack the new builtin.
46+ Ok(s) => format!("blob {}", text_literal(&s)),
110− if interp.globals.lookup("blobHex").is_none() {
47+ Err(_) => "blob \"<binary>\"".into(),
111− return Err(Crash::new(
48+ },
112− "show: binary, executable, and symlink blobs require `blobHex : Text -> Text -> Blob` in config.j",
49+ BlobContent::Lazy(_) | BlobContent::Conflict(_) => {
113− ));
50+ let s = b
114− }
51+ .bytes()
115− let kind = match b.kind {
52+ .map(|v| String::from_utf8_lossy(&v).to_string())
116− BlobKind::Regular => "regular",
53+ .unwrap_or_else(|_| "<unreadable>".into());
117− BlobKind::Executable => "executable",
54+ let marker = if b.is_unresolved() { "{- unresolved -} " } else { "" };
118− BlobKind::Symlink => "symlink",
55+ format!("{}blob {}", marker, text_literal(&s))
119− };
56+ }
120− out.push_str("blobHex ");
57+ },
121− push_text_literal(kind, out);
58+ Value::Shape(s) => s.name.clone(),
122− out.push(' ');
59+ Value::Fun(f) => render_fun(interp, f),
123− push_text_literal(&hex::encode(bytes.as_ref()), out);
124− Ok(())
125−}
126−
127−/// A lazy part (a commit's `files`) renders as the value it forces to: on
128−/// one line, or broken as that value would be at the indentation `wide`
129−/// gives.
130−#[inline(never)]
131−fn render_forced(interp: &Interp, t: &ThunkVal, wide: Option<usize>, out: &mut String) -> Result<(), Crash> {
132− let v = t.force()?;
133− match wide {
134− None => render(interp, &v, out),
135− Some(indent) => render_wide(interp, &v, indent, out),
136 }
60 }
137}
61}
138
62
139−#[inline(never)]
63+fn render_fun(interp: &Interp, f: &FunVal) -> String {
140−fn render_fun(interp: &Interp, f: &FunVal, out: &mut String) -> Result<(), Crash> {
141 match f {
64 match f {
142− // a signed definition whose value is a builtin's partial application
143− // or a composition (`tree = treeWith { … }`) renders as its name,
144− // followed only by the arguments it is given (§5.2), not those baked
145− // into its value: all it was given since it was named, which its
146− // signature stops counting at its end (`k3 ((+)) 0`, `k3 = foldl`)
147− FunVal::Builtin {
148− name,
149− args,
150− pending: Some((cname, _, named_at)),
151− ..
152− } if interp.builtin_def(name, cname).is_some() => {
153− out.push_str(cname);
154− render_args(interp, &args[(*named_at).min(args.len())..], out)
155− }
156 FunVal::Builtin { name, args, .. } => {
65 FunVal::Builtin { name, args, .. } => {
157− // a selector `.name` carries its field as a baked-in first
66+ // operator builtins render parenthesised when bare
158− // argument (builtins::make_selector) and renders as the selector
67+ if args.is_empty() {
159− // atom itself (§3.4), not as an application to that field
68+ if is_operator_name(name) {
160− if name.starts_with('.') && name.len() > 1 {
69+ format!("({})", name)
161− out.push_str(name);
70+ } else if name.starts_with('.') && name.len() > 1 {
162− return Ok(());
71+ name.clone() // selector .name
163− }
72+ } else if name == "(.)" {
164− // operator builtins render parenthesised; `(.)` is its own name
73+ "(.)".into()
165− if is_operator_name(name) {
74+ } else {
166− out.push('(');
75+ name.clone()
167− out.push_str(name);
168− out.push(')');
169− } else {
170− out.push_str(name);
171− }
172− // a partial application: the name followed by the applied args,
173− // which may contain a baked-in value (compose)
174− if name == "(.)" && args.len() == 2 {
175− for a in args {
176− out.push_str(" (");
177− render_atom(interp, a, out)?;
178− out.push(')');
179 }
76 }
180− Ok(())
181 } else {
77 } else {
182− render_args(interp, args, out)
78+ // partial application: name followed by applied args
79+ let base = if is_operator_name(name) {
80+ format!("({})", name)
81+ } else if name == "(.)" {
82+ "(.)".into()
83+ } else {
84+ name.clone()
85+ };
86+ // args may contain a baked-in value (compose, selectors)
87+ match name.as_str() {
88+ "(.)" if args.len() == 2 => {
89+ format!(
90+ "(.) ({}) ({})",
91+ render_atom(interp, &args[0]),
92+ render_atom(interp, &args[1])
93+ )
94+ }
95+ _ => {
96+ let shown: Vec<String> =
97+ args.iter().map(|a| render_atom(interp, a)).collect();
98+ format!("{} {}", base, shown.join(" "))
99+ }
100+ }
183 }
101 }
184 }
102 }
185 FunVal::Closure {
103 FunVal::Closure {
@@ -187,67 +105,46 @@ fn render_fun(interp: &Interp, f: &FunVal, out: &mut String) -> Result<(), Crash
187 applied_args,
105 applied_args,
188 ..
106 ..
189 } => {
107 } => {
190− // a partial application of a named definition: the name followed
108+ if applied_args.is_empty() {
191− // by the arguments supplied so far (§5.2)
109+ n.clone()
192− out.push_str(n);
110+ } else {
193− render_args(interp, applied_args, out)
111+ // partial application of a named definition: the name followed
112+ // by the arguments supplied so far (§5.2)
113+ let args: Vec<String> =
114+ applied_args.iter().map(|a| render_atom(interp, a)).collect();
115+ format!("{} {}", n, args.join(" "))
116+ }
194 }
117 }
195 FunVal::Closure {
118 FunVal::Closure {
196 name: None,
119 name: None,
197− applied_args,
120+ applied,
198 params,
121 params,
199 body,
122 body,
200 src,
123 src,
201 ..
124 ..
202 } => {
125 } => {
203− // a lambda or section renders as its source, exactly as written
126+ if *applied == 0 {
204− // (§5.2); only an AST built without source is re-rendered. A
127+ if src.is_empty() || src == "section" {
205− // partial application parenthesises it, as it is not atomic, and
128+ crate::parse::render_lambda(params, body)
206− // follows it with the arguments supplied so far (§5.2)
129+ } else {
207− let applied = !applied_args.is_empty();
130+ src.clone()
208− if applied {
131+ }
209− out.push('(');
210− }
211− if src.as_str().is_empty() {
212− out.push_str(&crate::parse::render_lambda(params, body));
213 } else {
132 } else {
214− out.push_str(src.as_str());
133+ let base = if src.is_empty() || src == "section" {
134+ crate::parse::render_lambda(params, body)
135+ } else {
136+ src.clone()
137+ };
138+ format!("({}) <applied>", base)
215 }
139 }
216− if applied {
217− out.push(')');
218− }
219− render_args(interp, applied_args, out)
220− }
221− FunVal::OrFun(a, b) => render_infix(interp, a, " or ", b, out),
222− FunVal::Labelled(n, _) => {
223− out.push('%');
224− out.push_str(n);
225− Ok(())
226 }
140 }
227− FunVal::ComposeLazy(f, g) => render_infix(interp, f, " . ", g, out),
141+ FunVal::OrFun(a, b) => format!("({} or {})", render_atom(interp, a), render_atom(interp, b)),
228− }
142+ FunVal::Labelled(n, _) => format!("%{}", n),
229−}
143+ FunVal::ComposeLazy(f, g) => format!("({} . {})", render_atom(interp, f), render_atom(interp, g)),
230−
231−/// `(a op b)`, for the functions an `or` or a composition builds
232−fn render_infix(interp: &Interp, a: &Value, op: &str, b: &Value, out: &mut String) -> Result<(), Crash> {
233− out.push('(');
234− render_atom(interp, a, out)?;
235− out.push_str(op);
236− render_atom(interp, b, out)?;
237− out.push(')');
238− Ok(())
239−}
240−
241−/// The arguments of a partial application, each after a space.
242−fn render_args(interp: &Interp, args: &[Value], out: &mut String) -> Result<(), Crash> {
243− for a in args {
244− out.push(' ');
245− render_atom(interp, a, out)?;
246 }
144 }
247− Ok(())
248}
145}
249
146
250−fn render_atom(interp: &Interp, v: &Value, out: &mut String) -> Result<(), Crash> {
147+fn render_atom(interp: &Interp, v: &Value) -> String {
251 // parenthesise if not atomic; a nested list is self-delimiting (a `[`
148 // parenthesise if not atomic; a nested list is self-delimiting (a `[`
252 // cannot begin a postfix), but a record is not: `{ … }` following another
149 // cannot begin a postfix), but a record is not: `{ … }` following another
253 // element would parse as a record *update* on it (§3.4), so records keep
150 // element would parse as a record *update* on it (§3.4), so records keep
@@ -255,29 +152,12 @@ fn render_atom(interp: &Interp, v: &Value, out: &mut String) -> Result<(), Crash
255 let atomic = matches!(
152 let atomic = matches!(
256 v,
153 v,
257 Value::Int(_) | Value::Text(_) | Value::Bool(_) | Value::Id(_) | Value::Shape(_) | Value::List(_)
154 Value::Int(_) | Value::Text(_) | Value::Bool(_) | Value::Id(_) | Value::Shape(_) | Value::List(_)
258− ) || is_section(v);
155+ );
259− if !atomic {
156+ let s = render(interp, v);
260− out.push('(');
157+ if atomic {
261− }
158+ s
262− render(interp, v, out)?;
159+ } else {
263− if !atomic {
160+ format!("({})", s)
264− out.push(')');
265− }
266− Ok(())
267−}
268−
269−/// A section renders as its source, `(op e)` or `(e op)` (§5.2), which is
270−/// already an atom (§3.4) and takes no parentheses of its own. A lambda's
271−/// source begins with its `\` instead, and a section, taking one argument,
272−/// is never partially applied.
273−fn is_section(v: &Value) -> bool {
274− match v {
275− Value::Fun(f) => matches!(
276− f.as_ref(),
277− FunVal::Closure { name: None, applied_args, src, .. }
278− if applied_args.is_empty() && src.as_str().starts_with('(')
279− ),
280− _ => false,
281 }
161 }
282}
162}
283
163
@@ -301,13 +181,7 @@ fn is_operator_name(n: &str) -> bool {
301}
181}
302
182
303pub fn text_literal(s: &str) -> String {
183pub fn text_literal(s: &str) -> String {
304− let mut out = String::new();
184+ let mut out = String::from("\"");
305− push_text_literal(s, &mut out);
306− out
307−}
308−
309−fn push_text_literal(s: &str, out: &mut String) {
310− out.push('"');
311 for c in s.chars() {
185 for c in s.chars() {
312 match c {
186 match c {
313 '"' => out.push_str("\\\""),
187 '"' => out.push_str("\\\""),
@@ -319,22 +193,21 @@ fn push_text_literal(s: &str, out: &mut String) {
319 }
193 }
320 }
194 }
321 out.push('"');
195 out.push('"');
196+ out
322}
197}
323
198
324−fn render_wide(interp: &Interp, v: &Value, indent: usize, out: &mut String) -> Result<(), Crash> {
199+fn render_wide(interp: &Interp, v: &Value, indent: usize) -> String {
325 match v {
200 match v {
326− // see through a lazy `files` list as `render` does, so that it breaks
327− // like the equal forced list instead of staying on one line
328− Value::Thunk(t) => return render_forced(interp, t, Some(indent), out),
329 Value::List(xs) if !xs.is_empty() => {
201 Value::List(xs) if !xs.is_empty() => {
330− out.push('[');
202+ let mut out = String::from("[");
331 for (i, x) in xs.iter().enumerate() {
203 for (i, x) in xs.iter().enumerate() {
332 if i > 0 {
204 if i > 0 {
333 out.push('\n');
205 out.push('\n');
334 out.push_str(&" ".repeat(indent + 2));
206 out.push_str(&" ".repeat(indent + 2));
335 }
207 }
336 // list elements must be atoms (§3.4)
208 // list elements must be atoms (§3.4)
337− let atomic = matches!(
209+ let s = render_wide(interp, x, indent + 2);
210+ if matches!(
338 x,
211 x,
339 Value::Int(_)
212 Value::Int(_)
340 | Value::Text(_)
213 | Value::Text(_)
@@ -343,34 +216,30 @@ fn render_wide(interp: &Interp, v: &Value, indent: usize, out: &mut String) -> R
343 | Value::Shape(_)
216 | Value::Shape(_)
344 | Value::List(_)
217 | Value::List(_)
345 | Value::Record(_)
218 | Value::Record(_)
346− ) || is_section(x);
219+ ) {
347− if !atomic {
220+ out.push_str(&s);
348− out.push('(');
221+ } else {
349− }
222+ out.push_str(&format!("({})", s));
350− render_wide(interp, x, indent + 2, out)?;
351− if !atomic {
352− out.push(')');
353 }
223 }
354 }
224 }
355 out.push(']');
225 out.push(']');
226+ out
356 }
227 }
357 Value::Record(m) if !m.is_empty() => {
228 Value::Record(m) if !m.is_empty() => {
358− out.push_str("{ ");
229+ let mut out = String::from("{ ");
359 for (i, (k, x)) in m.iter().enumerate() {
230 for (i, (k, x)) in m.iter().enumerate() {
360 if i > 0 {
231 if i > 0 {
361 out.push('\n');
232 out.push('\n');
362 out.push_str(&" ".repeat(indent));
233 out.push_str(&" ".repeat(indent));
363 out.push_str(", ");
234 out.push_str(", ");
364 }
235 }
365− out.push_str(k);
236+ out.push_str(&format!("{} = {}", k, render_wide(interp, x, indent + 2)));
366− out.push_str(" = ");
367− render_wide(interp, x, indent + 2, out)?;
368 }
237 }
369 out.push_str(" }");
238 out.push_str(" }");
239+ out
370 }
240 }
371− _ => return render(interp, v, out),
241+ _ => render(interp, v),
372 }
242 }
373− Ok(())
374}
243}
375
244
376// ----------------------------------------------------------------------
245// ----------------------------------------------------------------------
@@ -378,38 +247,10 @@ fn render_wide(interp: &Interp, v: &Value, indent: usize, out: &mut String) -> R
378// ----------------------------------------------------------------------
247// ----------------------------------------------------------------------
379
248
380pub fn unified_diff(a: &str, b: &str) -> String {
249pub fn unified_diff(a: &str, b: &str) -> String {
381− // lines end after each `\n` and nowhere else, as for GNU diff and git:
250+ let diff = similar::TextDiff::from_lines(a, b);
382− // `similar`'s own line splitting also ends one at a lone `\r`, so a last
383− // line `b\r` counted as terminated and got no marker below
384− let old: Vec<&str> = a.split_inclusive('\n').collect();
385− let new: Vec<&str> = b.split_inclusive('\n').collect();
386− let diff = similar::TextDiff::configure().diff_slices(&old, &new);
387 let mut out = String::new();
251 let mut out = String::new();
388 for hunk in diff.unified_diff().context_radius(3).iter_hunks() {
252 for hunk in diff.unified_diff().context_radius(3).iter_hunks() {
389− // each side's range is the lines of it the hunk shows. `hunk.header()`
253+ out.push_str(&format!("{}\n", hunk.header()));
390− // took a side's start from the hunk's first op and its end from the
391− // last, but `similar`'s compaction moves ops past one another without
392− // updating the index each keeps on the side it does not touch, so the
393− // counts could disagree with the body ("@@ -1 +2,2 @@" over two old
394− // lines and three new), which `patch` and `git apply` refuse. A
395− // change's index on its own side is right, and each side's lines
396− // come in order.
397− let (mut old_first, mut old_len, mut new_first, mut new_len) = (None, 0, None, 0);
398− for change in hunk.iter_changes() {
399− if let Some(i) = change.old_index() {
400− old_first.get_or_insert(i);
401− old_len += 1;
402− }
403− if let Some(i) = change.new_index() {
404− new_first.get_or_insert(i);
405− new_len += 1;
406− }
407− }
408− out.push_str(&format!(
409− "@@ -{} +{} @@\n",
410− hunk_range(old_first, old_len),
411− hunk_range(new_first, new_len)
412− ));
413 for change in hunk.iter_changes() {
254 for change in hunk.iter_changes() {
414 let sign = match change.tag() {
255 let sign = match change.tag() {
415 similar::ChangeTag::Delete => "-",
256 similar::ChangeTag::Delete => "-",
@@ -417,33 +258,19 @@ pub fn unified_diff(a: &str, b: &str) -> String {
417 similar::ChangeTag::Equal => " ",
258 similar::ChangeTag::Equal => " ",
418 };
259 };
419 out.push_str(sign);
260 out.push_str(sign);
420− let text: &str = change.value();
261+ let text = change.to_string();
421− out.push_str(text);
262+ // similar yields the line with its terminator when present; the
422− // a line with no terminator, only ever the last, is ended here
263+ // last line of a file without a trailing newline has none, so add
423− // and marked, as GNU diff and git do: otherwise dropping the
264+ // one to keep the diff line-oriented
424− // final newline shows as the same line removed and re-added,
265+ out.push_str(&text);
425− // alike in both directions
426 if !text.ends_with('\n') {
266 if !text.ends_with('\n') {
427 out.push('\n');
267 out.push('\n');
428− out.push_str("\\ No newline at end of file\n");
429 }
268 }
430 }
269 }
431 }
270 }
432 out
271 out
433}
272}
434
273
435−/// One side's range in a hunk header as `diff -u` writes it, from the index
436−/// of the side's first line in the hunk and how many it has: `s` for one
437−/// line, `s,n` otherwise. With context around every change a hunk shows no
438−/// line of a side only when that side has none, the empty range `0,0`.
439−fn hunk_range(first: Option<usize>, len: usize) -> String {
440− match first {
441− Some(i) if len == 1 => format!("{}", i + 1),
442− Some(i) => format!("{},{}", i + 1, len),
443− None => "0,0".to_string(),
444− }
445−}
446−
447// ----------------------------------------------------------------------
274// ----------------------------------------------------------------------
448// difftastic (§7.10)
275// difftastic (§7.10)
449// ----------------------------------------------------------------------
276// ----------------------------------------------------------------------
@@ -455,13 +282,6 @@ pub fn difft(path: &Value, a: &Value, b: &Value) -> Result<Value, Crash> {
455 .map(|c| c.as_text().map(|s| s.to_string()))
282 .map(|c| c.as_text().map(|s| s.to_string()))
456 .transpose()?
283 .transpose()?
457 .unwrap_or_else(|| "file".to_string());
284 .unwrap_or_else(|| "file".to_string());
458− // A Path value can be built directly in the language. Only a basename
459− // may be joined below the temporary directories: an absolute path or
460− // a component containing separators must not escape them.
461− let name = std::path::Path::new(&last);
462− if name.file_name() != Some(name.as_os_str()) {
463− return Err(Crash::new("difft: the last path component must be a filename"));
464− }
465 let a_bytes = match a {
285 let a_bytes = match a {
466 Value::Blob(bl) => bl.bytes()?,
286 Value::Blob(bl) => bl.bytes()?,
467 v => return Err(Crash::new(format!("difft: expected a Blob, got a {}", v.kind_name()))),
287 v => return Err(Crash::new(format!("difft: expected a Blob, got a {}", v.kind_name()))),
@@ -472,22 +292,13 @@ pub fn difft(path: &Value, a: &Value, b: &Value) -> Result<Value, Crash> {
472 };
292 };
473 let dir = tempfile::tempdir()
293 let dir = tempfile::tempdir()
474 .map_err(|e| Crash::new(format!("difft: cannot create temp dir: {}", e)))?;
294 .map_err(|e| Crash::new(format!("difft: cannot create temp dir: {}", e)))?;
475− // Keep the filename unchanged: adding a prefix can exceed NAME_MAX for
295+ let old = dir.path().join(format!("old-{}", last));
476− // a valid tracked path, and some languages are detected by exact name.
296+ let new = dir.path().join(format!("new-{}", last));
477− let old_dir = dir.path().join("old");
478− let new_dir = dir.path().join("new");
479− std::fs::create_dir(&old_dir).map_err(|e| Crash::new(format!("difft: {}", e)))?;
480− std::fs::create_dir(&new_dir).map_err(|e| Crash::new(format!("difft: {}", e)))?;
481− let old = old_dir.join(name);
482− let new = new_dir.join(name);
483 std::fs::write(&old, &a_bytes).map_err(|e| Crash::new(format!("difft: {}", e)))?;
297 std::fs::write(&old, &a_bytes).map_err(|e| Crash::new(format!("difft: {}", e)))?;
484 std::fs::write(&new, &b_bytes).map_err(|e| Crash::new(format!("difft: {}", e)))?;
298 std::fs::write(&new, &b_bytes).map_err(|e| Crash::new(format!("difft: {}", e)))?;
485 let mut cmd = std::process::Command::new("difft");
299 let mut cmd = std::process::Command::new("difft");
486 cmd.arg(&old).arg(&new);
300 cmd.arg(&old).arg(&new);
487 cmd.stdin(std::process::Stdio::null());
301 cmd.stdin(std::process::Stdio::null());
488− // stdout is the rendering; what difft says on stderr is j's, so one
489− // that fails explains itself rather than leaving a blank page (§7.10)
490− cmd.stderr(std::process::Stdio::inherit());
491 if std::env::var_os("DFT_COLOR").is_none() && stdout_is_tty() {
302 if std::env::var_os("DFT_COLOR").is_none() && stdout_is_tty() {
492 cmd.env("DFT_COLOR", "always");
303 cmd.env("DFT_COLOR", "always");
493 }
304 }
@@ -496,48 +307,12 @@ pub fn difft(path: &Value, a: &Value, b: &Value) -> Result<Value, Crash> {
496 cmd.env("DFT_WIDTH", w.to_string());
307 cmd.env("DFT_WIDTH", w.to_string());
497 }
308 }
498 }
309 }
499− crate::terminal::external_stderr();
310+ let out = cmd
500− let out = match cmd.output() {
311+ .output()
501− Ok(out) => out,
312+ .map_err(|_| Crash::new("difft: cannot execute `difft` (not on PATH)"))?;
502− Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
503− return fallback_diff(comps, &a_bytes, &b_bytes);
504− }
505− Err(error) => return Err(Crash::new(format!("difft: cannot execute `difft`: {}", error))),
506− };
507 Ok(Value::text(String::from_utf8_lossy(&out.stdout).to_string()))
313 Ok(Value::text(String::from_utf8_lossy(&out.stdout).to_string()))
508}
314}
509
315
510−/// Keep review usable without an optional renderer. Include the full path so
511−/// concatenating several fallback diffs never loses which file each belongs to.
512−fn fallback_diff(path: &[Value], a: &[u8], b: &[u8]) -> Result<Value, Crash> {
513− if a == b {
514− return Ok(Value::text(""));
515− }
516− let path = if path.is_empty() {
517− "file".to_string()
518− } else {
519− path.iter().map(|part| part.as_text().map(|s| s.to_string()))
520− .collect::<Result<Vec<_>, _>>()?.join("/")
521− };
522− let label = |prefix: &str| {
523− let label = format!("{prefix}/{path}");
524− if label.chars().any(|c| c.is_whitespace() || c.is_control() || matches!(c, '"' | '\\')) {
525− format!("{label:?}")
526− } else {
527− label
528− }
529− };
530− let old = label("a");
531− let new = label("b");
532− let body = match (std::str::from_utf8(a), std::str::from_utf8(b)) {
533− (Ok(a), Ok(b)) if !a.contains('\0') && !b.contains('\0') => {
534− format!("--- {old}\n+++ {new}\n{}", unified_diff(a, b))
535− }
536− _ => format!("Binary files {old} and {new} differ\n"),
537− };
538− Ok(Value::text(body))
539−}
540−
541pub fn stdout_is_tty() -> bool {
316pub fn stdout_is_tty() -> bool {
542 unsafe { libc::isatty(libc::STDOUT_FILENO) == 1 }
317 unsafe { libc::isatty(libc::STDOUT_FILENO) == 1 }
543}
318}
src/terminal.rsdeleted−276
@@ -1,276 +0,0 @@
1−//! Interactive presentation. Rendered values stay free of cursor controls;
2−//! motion belongs only to the CLI's final output and temporary status line.
3−
4−use std::fmt;
5−use std::io::{self, IsTerminal, Write};
6−use std::sync::atomic::{AtomicBool, Ordering};
7−use std::sync::mpsc::{self, Sender};
8−use std::sync::{Arc, Mutex, Weak};
9−use std::thread::{self, JoinHandle};
10−use std::time::{Duration, Instant};
11−
12−use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
13−
14−static ACTIVE_PROGRESS: Mutex<Option<Weak<ProgressWorker>>> = Mutex::new(None);
15−static EXTERNAL_STDERR: AtomicBool = AtomicBool::new(false);
16−
17−fn stop_progress() {
18− let worker = ACTIVE_PROGRESS.lock().unwrap().as_ref().and_then(Weak::upgrade);
19− if let Some(worker) = worker {
20− worker.finish();
21− }
22−}
23−
24−/// A subprocess may write an unterminated diagnostic. Clear our status before
25−/// it starts, and leave stderr alone for the remainder of this run.
26−pub fn external_stderr() {
27− EXTERNAL_STDERR.store(true, Ordering::Relaxed);
28− stop_progress();
29−}
30−
31−fn nonempty_env(name: &str) -> bool {
32− std::env::var_os(name).is_some_and(|value| !value.is_empty())
33−}
34−
35−fn capable_terminal() -> bool {
36− !nonempty_env("NO_COLOR") && std::env::var("TERM").as_deref() != Ok("dumb")
37−}
38−
39−fn interactive() -> bool {
40− io::stdout().is_terminal()
41− && io::stderr().is_terminal()
42− && !nonempty_env("CI")
43−}
44−
45−fn motion_enabled() -> bool {
46− interactive()
47− && capable_terminal()
48− && !matches!(
49− std::env::var("J_ANIMATIONS")
50− .unwrap_or_default()
51− .to_ascii_lowercase()
52− .as_str(),
53− "never" | "off" | "false" | "0"
54− )
55−}
56−
57−/// Write a diagnostic without turning a closed stderr into a panic (§1.4).
58−pub fn report(args: fmt::Arguments<'_>) {
59− report_with_color(args, "1;96");
60−}
61−
62−pub fn report_error(args: fmt::Arguments<'_>) {
63− report_with_color(args, "1;91");
64−}
65−
66−fn report_with_color(args: fmt::Arguments<'_>, code: &str) {
67− stop_progress();
68− let mut out = io::stderr().lock();
69− if out.is_terminal() && capable_terminal() {
70− let message = args.to_string();
71− if let Some(rest) = message.strip_prefix("j: ") {
72− let _ = writeln!(out, "\x1b[{code}mj:\x1b[0m {rest}");
73− } else {
74− let _ = writeln!(out, "\x1b[2m{message}\x1b[0m");
75− }
76− } else {
77− let _ = writeln!(out, "{args}");
78− }
79−}
80−
81−/// Short confirmation for commands that otherwise have no terminal output.
82−/// Pipes and unattended runs keep their existing, quiet behavior.
83−pub fn success(message: &str) {
84− stop_progress();
85− if interactive() {
86− let mut out = io::stderr().lock();
87− if capable_terminal() {
88− let _ = writeln!(out, "\x1b[1;92m✓\x1b[0m \x1b[1;96mj:\x1b[0m {message}");
89− } else {
90− let _ = writeln!(out, "j: {message}");
91− }
92− }
93−}
94−
95−/// A delayed spinner, activity bar, and elapsed time. The bar is deliberately
96−/// indeterminate: elapsed time cannot tell us how much work remains.
97−/// Stopping wakes the worker immediately, so short operations have no extra
98−/// delay. Never hide or move the cursor
99−/// between rows: even an abrupt signal leaves the terminal usable.
100−struct Progress {
101− worker: Option<Arc<ProgressWorker>>,
102−}
103−
104−struct ProgressWorker {
105− stop: Sender<()>,
106− thread: Mutex<Option<JoinHandle<()>>>,
107−}
108−
109−impl ProgressWorker {
110− fn finish(&self) {
111− let _ = self.stop.send(());
112− if let Some(worker) = self.thread.lock().unwrap().take() {
113− let _ = worker.join();
114− }
115− }
116−}
117−
118−impl Progress {
119− fn start(label: &'static str) -> Self {
120− if !motion_enabled() || EXTERNAL_STDERR.load(Ordering::Relaxed) {
121− return Self { worker: None };
122− }
123− stop_progress();
124− let (send, receive) = mpsc::channel();
125− let started = Instant::now();
126− let worker = thread::Builder::new().name("j-progress".into()).spawn(move || {
127− if receive.recv_timeout(Duration::from_millis(140)) != Err(mpsc::RecvTimeoutError::Timeout) {
128− return;
129− }
130− let colors = ["95", "96", "93", "92", "94"];
131− let mut frame = 0;
132− loop {
133− // Fit each new frame to the current terminal width, including
134− // after a resize. Leave one column spare at the edge.
135− let width = stderr_width().saturating_sub(1);
136− let line = progress_line(label, frame, started.elapsed(), width);
137− {
138− let mut out = io::stderr().lock();
139− if write!(
140− out,
141− "\r\x1b[2K\x1b[1;{}m{}\x1b[0m",
142− colors[frame % colors.len()],
143− line
144− )
145− .and_then(|()| out.flush())
146− .is_err()
147− {
148− break;
149− }
150− }
151− frame += 1;
152− if receive.recv_timeout(Duration::from_millis(75)) != Err(mpsc::RecvTimeoutError::Timeout) {
153− break;
154− }
155− }
156− let mut out = io::stderr().lock();
157− let _ = write!(out, "\r\x1b[2K").and_then(|()| out.flush());
158− });
159− let worker = worker.ok().map(|worker| Arc::new(ProgressWorker {
160− stop: send,
161− thread: Mutex::new(Some(worker)),
162− }));
163− *ACTIVE_PROGRESS.lock().unwrap() = worker.as_ref().map(Arc::downgrade);
164− Self { worker }
165− }
166−}
167−
168−impl Drop for Progress {
169− fn drop(&mut self) {
170− if let Some(worker) = self.worker.take() {
171− worker.finish();
172− }
173− }
174−}
175−
176−/// Finish the temporary line before the caller reports success or failure.
177−pub fn with_progress<T>(label: &'static str, operation: impl FnOnce() -> T) -> T {
178− let _progress = Progress::start(label);
179− operation()
180−}
181−
182−fn progress_line(label: &str, frame: usize, elapsed: Duration, width: usize) -> String {
183− const FRAMES: [char; 10] = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
184− let mut line = format!("{} j · {}", FRAMES[frame % FRAMES.len()], label);
185− let seconds = elapsed.as_secs();
186− let time = if seconds < 60 {
187− format!("{}.{}s", seconds, elapsed.subsec_millis() / 100)
188− } else {
189− format!("{}m {:02}s", seconds / 60, seconds % 60)
190− };
191− // Bounce a three-cell pulse across a ten-cell track. It never fills up
192− // or suggests a completion percentage for work with no known total.
193− let step = frame % 14;
194− let start = step.min(14 - step);
195− let bar: String = (0..10)
196− .map(|cell| if (start..start + 3).contains(&cell) { '━' } else { '·' })
197− .collect();
198− let details = format!(" [{bar}] {time}");
199− if line.width() + details.width() <= width {
200− line.push_str(&details);
201− } else if line.width() + 1 + time.width() <= width {
202− // On small terminals, keep the operation's name ahead of decoration.
203− line.push(' ');
204− line.push_str(&time);
205− }
206− let mut used = 0;
207− line.chars()
208− .take_while(|c| {
209− used += c.width().unwrap_or(0);
210− used <= width
211− })
212− .collect()
213−}
214−
215−fn stderr_width() -> usize {
216− // The two streams can be attached to different terminals. Measure the
217− // one that receives the progress line, rather than the tree's stdout.
218− unsafe {
219− let mut size: libc::winsize = std::mem::zeroed();
220− if libc::ioctl(libc::STDERR_FILENO, libc::TIOCGWINSZ, &mut size) == 0
221− && size.ws_col > 0
222− {
223− size.ws_col as usize
224− } else {
225− 80
226− }
227− }
228−}
229−
230−/// Reveal colored, multiline terminal output in a few quick batches. Never
231−/// split a line (and therefore an ANSI style), alter its bytes, or animate
232−/// plain text. The total pacing is at most 96 ms, even for very large trees.
233−pub fn write_output(out: &mut impl Write, text: &str) -> io::Result<()> {
234− if !motion_enabled() || !text.contains("\x1b[") {
235− return out.write_all(text.as_bytes()).and_then(|()| out.flush());
236− }
237− let lines = text.bytes().filter(|&byte| byte == b'\n').count()
238− + usize::from(!text.ends_with('\n'));
239− let batch = lines.div_ceil(7).max(1);
240− let mut start = 0;
241− let mut end = 0;
242− for (index, line) in text.split_inclusive('\n').enumerate() {
243− end += line.len();
244− if (index + 1) % batch == 0 && index + 1 < lines {
245− out.write_all(text[start..end].as_bytes())?;
246− out.flush()?;
247− start = end;
248− thread::sleep(Duration::from_millis(16));
249− }
250− }
251− out.write_all(text[start..].as_bytes())?;
252− out.flush()
253−}
254−
255−#[cfg(test)]
256−mod tests {
257− use super::*;
258−
259− #[test]
260− fn progress_fits_terminal_cells_and_keeps_the_operation_visible() {
261− let label = "Reading 仓库";
262− for width in 0..80 {
263− for frame in 0..20 {
264− let line = progress_line(label, frame, Duration::from_millis(1234), width);
265− assert!(line.width() <= width, "{width} columns: {line:?}");
266− if width >= 18 {
267− assert!(line.contains(label), "operation name lost: {line:?}");
268− }
269− }
270− }
271− let line = progress_line("Reading repository", 0, Duration::from_millis(1234), 80);
272− assert!(line.contains("[━━━·······] 1.2s"), "{line:?}");
273− assert!(progress_line("Reading repository", 0, Duration::from_secs(65), 80)
274− .ends_with("1m 05s"));
275− }
276−}
src/value.rs+86−637
@@ -3,7 +3,7 @@
3use crate::ast::{Expr, Pattern};
3use crate::ast::{Expr, Pattern};
4use num_bigint::BigInt;
4use num_bigint::BigInt;
5use std::collections::{BTreeMap, BTreeSet, HashMap};
5use std::collections::{BTreeMap, BTreeSet, HashMap};
6−use std::rc::{Rc, Weak};
6+use std::rc::Rc;
7
7
8pub type RecordMap = BTreeMap<String, Value>;
8pub type RecordMap = BTreeMap<String, Value>;
9
9
@@ -53,17 +53,6 @@ impl ListVal {
53 pub fn as_slice(&self) -> &[Value] {
53 pub fn as_slice(&self) -> &[Value] {
54 &self.items[self.start..]
54 &self.items[self.start..]
55 }
55 }
56− /// which list this is, while it is held: two lists of one identity
57− /// share their elements from the same first one, so are the same list
58− /// without either being compared
59− pub fn identity(&self) -> (*const Vec<Value>, usize) {
60− (Rc::as_ptr(&self.items), self.start)
61− }
62−
63− /// Observe the shared storage without keeping a cached list alive.
64− pub(crate) fn weak_storage(&self) -> std::rc::Weak<Vec<Value>> {
65− Rc::downgrade(&self.items)
66− }
67}
56}
68
57
69impl std::ops::Deref for ListVal {
58impl std::ops::Deref for ListVal {
@@ -75,38 +64,18 @@ impl std::ops::Deref for ListVal {
75
64
76pub struct ThunkVal {
65pub struct ThunkVal {
77 state: std::cell::RefCell<ThunkState>,
66 state: std::cell::RefCell<ThunkState>,
78− /// for a commit's `files` as the backend built them: the change id of the
79− /// stored commit whose tree this loads. An edit that changes the files
80− /// replaces the thunk, so a commit still holding the one tagged with its
81− /// own id holds exactly the stored tree — the only case in which the
82− /// backend's answers about that tree describe the value (§7.2).
83− origin: Option<String>,
84− /// for a commit's `files` as the backend built them: a name for the
85− /// stored tree this loads. Two lists loaded under the same name are
86− /// equal, and a list the backend loads from a tree is a snapshot (§7.3),
87− /// so persisting compares and validates a commit the program left as
88− /// loaded without reading its files (§7.5).
89− tree: Option<String>,
90}
67}
91
68
92enum ThunkState {
69enum ThunkState {
93 Pending(Box<dyn FnOnce() -> Result<Value, Crash>>),
70 Pending(Box<dyn FnOnce() -> Result<Value, Crash>>),
94− /// the computation has been taken out of `Pending` and is running
95− Forcing,
96 Ready(Value),
71 Ready(Value),
97− /// the computation crashed (a store read failed). Every later force
98− /// crashes the same way (§7.2): a crash caught by `or`, or swallowed by
99− /// rendering, must not leave some other value behind.
100− Failed(Crash),
101}
72}
102
73
103impl std::fmt::Debug for ThunkVal {
74impl std::fmt::Debug for ThunkVal {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 match &*self.state.borrow() {
76 match &*self.state.borrow() {
106 ThunkState::Pending(_) => write!(f, "Thunk(pending)"),
77 ThunkState::Pending(_) => write!(f, "Thunk(pending)"),
107− ThunkState::Forcing => write!(f, "Thunk(forcing)"),
108 ThunkState::Ready(v) => write!(f, "Thunk({:?})", v),
78 ThunkState::Ready(v) => write!(f, "Thunk({:?})", v),
109− ThunkState::Failed(c) => write!(f, "Thunk(failed: {})", c.msg),
110 }
79 }
111 }
80 }
112}
81}
@@ -115,64 +84,27 @@ impl ThunkVal {
115 pub fn new(f: impl FnOnce() -> Result<Value, Crash> + 'static) -> Self {
84 pub fn new(f: impl FnOnce() -> Result<Value, Crash> + 'static) -> Self {
116 ThunkVal {
85 ThunkVal {
117 state: std::cell::RefCell::new(ThunkState::Pending(Box::new(f))),
86 state: std::cell::RefCell::new(ThunkState::Pending(Box::new(f))),
118− origin: None,
119− tree: None,
120 }
87 }
121 }
88 }
122
89
123− /// the lazy `files` of the stored commit with change id `origin`, which
90+ /// the value, computing it on first call and memoizing
124− /// load the stored tree named `tree`, if the backend names its trees
125− pub fn stored(
126− origin: String,
127− tree: Option<String>,
128− f: impl FnOnce() -> Result<Value, Crash> + 'static,
129− ) -> Self {
130− ThunkVal {
131− origin: Some(origin),
132− tree,
133− ..ThunkVal::new(f)
134− }
135− }
136−
137− /// the change id of the stored commit whose tree this loads, if any
138− pub fn origin(&self) -> Option<&str> {
139− self.origin.as_deref()
140− }
141−
142− /// the name of the stored tree this loads, if the backend gave one
143− pub fn tree(&self) -> Option<&str> {
144− self.tree.as_deref()
145− }
146−
147− /// the value, where it has been computed; nothing is computed here
148− pub fn peek(&self) -> Option<Value> {
149− match &*self.state.borrow() {
150− ThunkState::Ready(v) => Some(v.clone()),
151− _ => None,
152− }
153− }
154−
155− /// the value, computing it on first call and memoizing the outcome, a
156− /// crash included
157 pub fn force(&self) -> Result<Value, Crash> {
91 pub fn force(&self) -> Result<Value, Crash> {
158− match &*self.state.borrow() {
92+ if let ThunkState::Ready(v) = &*self.state.borrow() {
159− ThunkState::Ready(v) => return Ok(v.clone()),
93+ return Ok(v.clone());
160− ThunkState::Failed(c) => return Err(c.clone()),
94+ }
161− ThunkState::Forcing => {
95+ let thunk = {
162− return Err(Crash::new("internal: a lazy value was forced re-entrantly"))
96+ let mut st = self.state.borrow_mut();
97+ match std::mem::replace(&mut *st, ThunkState::Ready(Value::Bool(false))) {
98+ ThunkState::Pending(t) => t,
99+ ThunkState::Ready(v) => {
100+ *st = ThunkState::Ready(v.clone());
101+ return Ok(v);
102+ }
163 }
103 }
164− ThunkState::Pending(_) => {}
165− }
166− let thunk = match std::mem::replace(&mut *self.state.borrow_mut(), ThunkState::Forcing) {
167− ThunkState::Pending(t) => t,
168− _ => unreachable!("the state was Pending just above"),
169 };
104 };
170− let out = thunk();
105+ let v = thunk()?;
171− *self.state.borrow_mut() = match &out {
106+ *self.state.borrow_mut() = ThunkState::Ready(v.clone());
172− Ok(v) => ThunkState::Ready(v.clone()),
107+ Ok(v)
173− Err(c) => ThunkState::Failed(c.clone()),
174− };
175− out
176 }
108 }
177}
109}
178
110
@@ -203,31 +135,8 @@ pub enum BlobContent {
203 Lazy(Rc<LazyBlob>),
135 Lazy(Rc<LazyBlob>),
204 /// Conflict sides, jj order: alternating adds and removes, starting and
136 /// Conflict sides, jj order: alternating adds and removes, starting and
205 /// ending with an add: [add, (remove, add)*]. For the in-memory backend:
137 /// ending with an add: [add, (remove, add)*]. For the in-memory backend:
206− /// [to, from, onto]-style three sides as [add, remove, add]. `None` is a
138+ /// [to, from, onto]-style three sides as [add, remove, add].
207− /// side on which the path is absent (a deletion conflict, §7.3).
139+ Conflict(Vec<Rc<Vec<u8>>>),
208− Conflict(Vec<Option<ConflictSide>>),
209−}
210−
211−/// One present side of a conflict: its content and its own file type, which
212−/// jj keeps per side (§7.3)
213−#[derive(Clone, Debug, PartialEq, Eq)]
214−pub struct ConflictSide {
215− pub kind: BlobKind,
216− pub bytes: Rc<Vec<u8>>,
217− /// a side on which jj holds a directory, in a conflict between a file
218− /// and a directory: the hex id of that tree, kept opaque so the side is
219− /// written back as the directory it was. Its `bytes` are empty (§7.3).
220− pub tree: Option<String>,
221−}
222−
223−impl ConflictSide {
224− pub fn regular(bytes: &[u8]) -> ConflictSide {
225− ConflictSide {
226− kind: BlobKind::Regular,
227− bytes: Rc::new(bytes.to_vec()),
228− tree: None,
229− }
230− }
231}
140}
232
141
233/// A blob whose bytes are read from the store only on first use (§7.2):
142/// A blob whose bytes are read from the store only on first use (§7.2):
@@ -240,12 +149,7 @@ pub struct LazyBlob {
240
149
241enum LazyState {
150enum LazyState {
242 Pending(Box<dyn FnOnce() -> Result<Rc<Vec<u8>>, Crash>>),
151 Pending(Box<dyn FnOnce() -> Result<Rc<Vec<u8>>, Crash>>),
243− /// the read has been taken out of `Pending` and is running
244− Forcing,
245 Ready(Rc<Vec<u8>>),
152 Ready(Rc<Vec<u8>>),
246− /// the read failed: every later force fails the same way, so the blob
247− /// never reads as empty (§7.2), whoever caught or swallowed the crash
248− Failed(Crash),
249}
153}
250
154
251impl std::fmt::Debug for LazyBlob {
155impl std::fmt::Debug for LazyBlob {
@@ -272,28 +176,25 @@ impl LazyBlob {
272 }
176 }
273 }
177 }
274
178
275− /// the bytes, reading from the store on first call and memoizing the
179+ /// the bytes, reading from the store on first call and memoizing
276− /// outcome, a failed read included
277 pub fn force(&self) -> Result<Rc<Vec<u8>>, Crash> {
180 pub fn force(&self) -> Result<Rc<Vec<u8>>, Crash> {
278 // fast path: already forced
181 // fast path: already forced
279− match &*self.state.borrow() {
182+ if let LazyState::Ready(b) = &*self.state.borrow() {
280− LazyState::Ready(b) => return Ok(b.clone()),
183+ return Ok(b.clone());
281− LazyState::Failed(c) => return Err(c.clone()),
184+ }
282− LazyState::Forcing => {
185+ let thunk = {
283− return Err(Crash::new("internal: a lazy blob was read re-entrantly"))
186+ let mut st = self.state.borrow_mut();
187+ match std::mem::replace(&mut *st, LazyState::Ready(Rc::new(Vec::new()))) {
188+ LazyState::Pending(t) => t,
189+ LazyState::Ready(b) => {
190+ *st = LazyState::Ready(b.clone());
191+ return Ok(b);
192+ }
284 }
193 }
285− LazyState::Pending(_) => {}
286− }
287− let thunk = match std::mem::replace(&mut *self.state.borrow_mut(), LazyState::Forcing) {
288− LazyState::Pending(t) => t,
289− _ => unreachable!("the state was Pending just above"),
290− };
291− let out = thunk();
292− *self.state.borrow_mut() = match &out {
293− Ok(b) => LazyState::Ready(b.clone()),
294− Err(c) => LazyState::Failed(c.clone()),
295 };
194 };
296− out
195+ let bytes = thunk()?;
196+ *self.state.borrow_mut() = LazyState::Ready(bytes.clone());
197+ Ok(bytes)
297 }
198 }
298}
199}
299
200
@@ -319,7 +220,7 @@ impl BlobVal {
319 match &self.content {
220 match &self.content {
320 BlobContent::Resolved(b) => b.len(),
221 BlobContent::Resolved(b) => b.len(),
321 BlobContent::Lazy(l) => l.force().map(|b| b.len()).unwrap_or(0),
222 BlobContent::Lazy(l) => l.force().map(|b| b.len()).unwrap_or(0),
322− BlobContent::Conflict(sides) => sides.iter().flatten().map(|s| s.bytes.len()).sum(),
223+ BlobContent::Conflict(sides) => sides.iter().map(|s| s.len()).sum(),
323 }
224 }
324 }
225 }
325 /// content as bytes, forcing a lazy blob; conflicts render with jj-style
226 /// content as bytes, forcing a lazy blob; conflicts render with jj-style
@@ -363,22 +264,25 @@ impl BlobVal {
363 }
264 }
364}
265}
365
266
366−/// Render a conflict with jj-style markers; a side the path is absent on
267+/// Render a conflict with jj-style markers.
367−/// renders as empty content, as jj materialises it.
268+pub fn render_conflict(sides: &[Rc<Vec<u8>>]) -> Vec<u8> {
368−pub fn render_conflict(sides: &[Option<ConflictSide>]) -> Vec<u8> {
369 // sides: [add0, remove0, add1, remove1, ... addN]
269 // sides: [add0, remove0, add1, remove1, ... addN]
370 let mut out = Vec::new();
270 let mut out = Vec::new();
371 out.extend_from_slice(b"<<<<<<<\n");
271 out.extend_from_slice(b"<<<<<<<\n");
372 let mut i = 0;
272 let mut i = 0;
273+ let mut first_add = true;
373 while i < sides.len() {
274 while i < sides.len() {
374− let bytes: &[u8] = sides[i].as_ref().map_or(&[], |s| &s.bytes);
375 if i % 2 == 0 {
275 if i % 2 == 0 {
276+ if !first_add {
277+ // another add after removes: separate
278+ }
376 out.extend_from_slice(b"+++++++\n");
279 out.extend_from_slice(b"+++++++\n");
377− out.extend_from_slice(bytes);
280+ out.extend_from_slice(&sides[i]);
378 ensure_newline(&mut out);
281 ensure_newline(&mut out);
282+ first_add = false;
379 } else {
283 } else {
380 out.extend_from_slice(b"%%%%%%%\n");
284 out.extend_from_slice(b"%%%%%%%\n");
381− out.extend_from_slice(bytes);
285+ out.extend_from_slice(&sides[i]);
382 ensure_newline(&mut out);
286 ensure_newline(&mut out);
383 }
287 }
384 i += 1;
288 i += 1;
@@ -418,6 +322,9 @@ impl PrimKind {
418pub enum ShapeKind {
322pub enum ShapeKind {
419 Record(BTreeSet<String>),
323 Record(BTreeSet<String>),
420 Prim(PrimKind),
324 Prim(PrimKind),
325+ /// an alias that could not be resolved to a record shape yet (function,
326+ /// list, or a typedecl processed later); resolved lazily at check time
327+ Aliased(String),
421}
328}
422
329
423#[derive(Clone, Debug)]
330#[derive(Clone, Debug)]
@@ -440,11 +347,7 @@ pub enum FunVal {
440 arity: usize,
347 arity: usize,
441 args: Vec<Value>,
348 args: Vec<Value>,
442 f: BuiltinFn,
349 f: BuiltinFn,
443− /// the name this function is under, its contract as further
350+ pending: Option<(String, Rc<crate::shape::ContractExpr>)>,
444− /// arguments arrive, and how many of `args` it held when it was
445− /// given that name, which are its value's, not the arguments a
446− /// signed definition was given (§5.2)
447− pending: Option<(String, Rc<crate::shape::ContractExpr>, usize)>,
448 },
351 },
449 Closure {
352 Closure {
450 name: Option<String>, // top-level definition name, if any
353 name: Option<String>, // top-level definition name, if any
@@ -452,94 +355,25 @@ pub enum FunVal {
452 applied: usize,
355 applied: usize,
453 /// the arguments supplied so far, for rendering (§5.2)
356 /// the arguments supplied so far, for rendering (§5.2)
454 applied_args: Vec<Value>,
357 applied_args: Vec<Value>,
358+ /// the body has not been evaluated yet (the definition's contract is
359+ /// not exhausted): the next application evaluates it, then applies
360+ deferred: bool,
455 body: Rc<Expr>,
361 body: Rc<Expr>,
456 env: Env,
362 env: Env,
457− /// the lambda's source, which it renders as (§5.2)
363+ src: String,
458− src: crate::ast::Source,
459 /// contract of this function as further arguments arrive
364 /// contract of this function as further arguments arrive
460 pending: Option<(String, Rc<crate::shape::ContractExpr>)>,
365 pending: Option<(String, Rc<crate::shape::ContractExpr>)>,
461 },
366 },
462 /// `f or g` lifted pointwise over functions (§4.6)
367 /// `f or g` lifted pointwise over functions (§4.6)
463 OrFun(Value, Value),
368 OrFun(Value, Value),
464− /// a composition `f . g` applied as `f (g x)` (§4.9); `(.)` itself
369+ /// unevaluated composition: unfolds only when applied to a non-function
465− /// builds a builtin node instead (builtins::compose_values)
370+ /// (§4.1 note — `abandon . contract everything` is a value before it is
371+ /// applied, and must not run at load)
466 ComposeLazy(Value, Value),
372 ComposeLazy(Value, Value),
467 /// a label literal %name: applies as `labelled "name"`
373 /// a label literal %name: applies as `labelled "name"`
468 Labelled(String, Value),
374 Labelled(String, Value),
469}
375}
470
376
471−/// What a function holds that may hold further functions: a closure's
472−/// environment and arguments, a builtin's arguments, the operands of an
473−/// `or` or a composition, a label's `labelled`. It is only ever dropped.
474−#[allow(dead_code)]
475−enum Held {
476− Closure(Env, Vec<Value>),
477− Args(Vec<Value>),
478− Two(Value, Value),
479− One(Value),
480−}
481−
482−/// How many function drops run inside one another before the next one's
483−/// contents are put off to the outermost.
484−const INLINE_FUN_DROPS: usize = 64;
485−
486−struct FunDrops {
487− depth: std::cell::Cell<usize>,
488− put_off: std::cell::RefCell<Vec<Held>>,
489−}
490−
491−thread_local! {
492− static FUN_DROPS: FunDrops = const {
493− FunDrops {
494− depth: std::cell::Cell::new(0),
495− put_off: std::cell::RefCell::new(Vec::new()),
496− }
497− };
498−}
499−
500−/// A function is dropped with what it holds, which may be a function holding
501−/// a function, and so on: a closure over a closure, a composition of
502−/// compositions, a named wrapper around another (named_apply). Dropped the
503−/// usual way, such a chain recursed once per link on the native stack, and a
504−/// chain a recursion had built a million links long aborted the process
505−/// (§4.1). Past a small depth, what a function holds is put off, and the
506−/// outermost function drop drops it, so the stack a drop takes is bounded.
507−impl Drop for FunVal {
508− fn drop(&mut self) {
509− let held = match self {
510− FunVal::Closure {
511− env, applied_args, ..
512− } => Held::Closure(std::mem::replace(env, Env::empty()), std::mem::take(applied_args)),
513− FunVal::Builtin { args, .. } => Held::Args(std::mem::take(args)),
514− FunVal::OrFun(a, b) | FunVal::ComposeLazy(a, b) => Held::Two(
515− std::mem::replace(a, Value::Bool(false)),
516− std::mem::replace(b, Value::Bool(false)),
517− ),
518− FunVal::Labelled(_, v) => Held::One(std::mem::replace(v, Value::Bool(false))),
519− };
520− // once the thread's locals are gone, `held` simply drops here
521− let _ = FUN_DROPS.try_with(move |d| {
522− let depth = d.depth.get();
523− if depth >= INLINE_FUN_DROPS {
524− d.put_off.borrow_mut().push(held);
525− return;
526− }
527− d.depth.set(depth + 1);
528− drop(held);
529− if depth == 0 {
530− loop {
531− let next = d.put_off.borrow_mut().pop();
532− match next {
533− Some(h) => drop(h),
534− None => break,
535− }
536− }
537− }
538− d.depth.set(depth);
539− });
540− }
541−}
542−
543impl Value {
377impl Value {
544 pub fn kind_name(&self) -> &'static str {
378 pub fn kind_name(&self) -> &'static str {
545 match self {
379 match self {
@@ -628,13 +462,17 @@ impl Value {
628
462
629/// Attach a definition name and contract to a function value (§4.13).
463/// Attach a definition name and contract to a function value (§4.13).
630pub fn attach_pending(v: &Value, name: &str, contract: Rc<crate::shape::Contract>) -> Value {
464pub fn attach_pending(v: &Value, name: &str, contract: Rc<crate::shape::Contract>) -> Value {
631− let cexpr = Rc::new(crate::shape::ContractExpr::Known { contract, at: 0 });
465+ let pending = Some((
632− let pending = Some((name.to_string(), cexpr.clone()));
466+ name.to_string(),
467+ Rc::new(crate::shape::ContractExpr::Known(contract)),
468+ ));
633 match v {
469 match v {
634 Value::Fun(fv) => match fv.as_ref() {
470 Value::Fun(fv) => match fv.as_ref() {
635 FunVal::Closure {
471 FunVal::Closure {
636 params,
472 params,
637 applied,
473 applied,
474+ applied_args,
475+ deferred,
638 body,
476 body,
639 env,
477 env,
640 src,
478 src,
@@ -643,10 +481,8 @@ pub fn attach_pending(v: &Value, name: &str, contract: Rc<crate::shape::Contract
643 name: Some(name.to_string()),
481 name: Some(name.to_string()),
644 params: params.clone(),
482 params: params.clone(),
645 applied: *applied,
483 applied: *applied,
646− // the definition renders as its name, followed only by the
484+ applied_args: applied_args.clone(),
647− // arguments it is given, not those its value holds: `prev`,
485+ deferred: *deferred,
648− // not `prev (parents)` (§5.2)
649− applied_args: Vec::new(),
650 body: body.clone(),
486 body: body.clone(),
651 env: env.clone(),
487 env: env.clone(),
652 src: src.clone(),
488 src: src.clone(),
@@ -663,81 +499,14 @@ pub fn attach_pending(v: &Value, name: &str, contract: Rc<crate::shape::Contract
663 arity: *arity,
499 arity: *arity,
664 args: args.clone(),
500 args: args.clone(),
665 f: *f,
501 f: *f,
666− // the arguments it holds now are its value's, and those
502+ pending,
667− // after them the definition's own: `k3 ((+)) 0` with
668− // `k3 = foldl`, past a signature that ends in a type
669− // variable and so stops counting (§5.2)
670− pending: Some((name.to_string(), cexpr, args.len())),
671 })),
503 })),
672− // `f or g` and `%name` have nowhere to hold a contract, so the
504+ FunVal::OrFun(_, _) | FunVal::ComposeLazy(_, _) | FunVal::Labelled(_, _) => v.clone(),
673− // definition becomes `\x -> v x` under its name, which the
674− // closure machinery checks at every argument the signature lists
675− // and at the result. The check is around the lifted `or` as a
676− // whole (§4.6): a result that violates the signature is a crash,
677− // not a reason to try the other side.
678− FunVal::OrFun(_, _) | FunVal::ComposeLazy(_, _) | FunVal::Labelled(_, _) => {
679− named_apply(v.clone(), Vec::new(), name, cexpr)
680− }
681 },
505 },
682 _ => v.clone(),
506 _ => v.clone(),
683 }
507 }
684}
508}
685
509
686−/// `\x -> f x` under a definition's name and contract: it renders as the
687−/// name followed by `applied_args` (§5.2), and its further arguments and its
688−/// result are checked against the contract (§4.13)
689−pub fn named_apply(
690− f: Value,
691− applied_args: Vec<Value>,
692− name: &str,
693− cexpr: Rc<crate::shape::ContractExpr>,
694−) -> Value {
695− Value::Fun(Rc::new(FunVal::Closure {
696− name: Some(name.to_string()),
697− params: vec![Pattern::Var("x".to_string())],
698− applied: 0,
699− applied_args,
700− body: NAMED_APPLY_BODY.with(Rc::clone),
701− env: Env::empty().extend(vec![("f".to_string(), f)]),
702− // a named closure renders by its name, not its source (§5.2)
703− src: crate::ast::Source::new(Rc::from(""), 0, 0),
704− pending: Some((name.to_string(), cexpr)),
705− }))
706−}
707−
708−thread_local! {
709− /// `f x`, the body every named_apply wrapper shares, by which one is
710− /// told from a lambda written in the language
711− static NAMED_APPLY_BODY: Rc<Expr> = {
712− let var = |n: &str| Rc::new(Expr::Var(n.to_string()));
713− Rc::new(Expr::App(var("f"), var("x")))
714− };
715−}
716−
717−/// The function `v` wraps when it is a named_apply wrapper under `name` and a
718−/// contract the same as `cexpr`. Naming that function afresh under them
719−/// checks all the wrapper would, so a definition that names the value of its
720−/// own recursive call wraps it once, not once per call (§4.13).
721−pub fn named_inner(v: &Value, name: &str, cexpr: &crate::shape::ContractExpr) -> Option<Value> {
722− match v {
723− Value::Fun(fv) => match fv.as_ref() {
724− FunVal::Closure {
725− body,
726− env,
727− pending: Some((n, c)),
728− ..
729− } if n == name
730− && c.same_as(cexpr)
731− && NAMED_APPLY_BODY.with(|b| Rc::ptr_eq(b, body)) =>
732− {
733− env.lookup("f")
734− }
735− _ => None,
736− },
737− _ => None,
738− }
739−}
740−
741/// A crash (§4.7).
510/// A crash (§4.7).
742#[derive(Debug, Clone)]
511#[derive(Debug, Clone)]
743pub struct Crash {
512pub struct Crash {
@@ -764,22 +533,6 @@ pub struct Env {
764 frame: Option<Rc<Frame>>,
533 frame: Option<Rc<Frame>>,
765}
534}
766
535
767−/// A large acyclic let or a collected recursive cycle can release a long
768−/// chain of parent frames. Release unique frames iteratively, keeping the
769−/// next parent alive while its child's values drop, so closures capturing
770−/// earlier frames cannot recurse down that chain either (§4.1).
771−impl Drop for Env {
772− fn drop(&mut self) {
773− let mut next = self.frame.take();
774− while let Some(frame) = next {
775− match Rc::try_unwrap(frame) {
776− Ok(mut frame) => next = frame.parent.frame.take(),
777− Err(_) => break,
778− }
779− }
780− }
781−}
782−
783pub struct Frame {
536pub struct Frame {
784 kind: FrameKind,
537 kind: FrameKind,
785 parent: Env,
538 parent: Env,
@@ -788,10 +541,9 @@ pub struct Frame {
788enum FrameKind {
541enum FrameKind {
789 Small(Vec<(String, Value)>),
542 Small(Vec<(String, Value)>),
790 Big(HashMap<String, Value>),
543 Big(HashMap<String, Value>),
791− /// recursive let bindings: the names the block binds, and their values
544+ /// recursive let bindings: filled incrementally, read through the cell so
792− /// in that order, filled incrementally and read through the cell so
793 /// closures capturing the frame see later bindings (§4.1)
545 /// closures capturing the frame see later bindings (§4.1)
794− Rec(Rc<[String]>, Rc<std::cell::RefCell<Vec<Value>>>),
546+ Rec(Rc<std::cell::RefCell<Vec<(String, Value)>>>),
795}
547}
796
548
797impl Env {
549impl Env {
@@ -817,23 +569,18 @@ impl Env {
817 }
569 }
818 }
570 }
819
571
820− /// extend with a recursive frame (for `let` blocks) binding `names`,
572+ /// extend with a recursive frame (for `let` blocks)
821− /// whose values are pushed to the returned cell in the same order
573+ pub fn extend_rec(&self) -> (Env, Rc<std::cell::RefCell<Vec<(String, Value)>>>) {
822− pub fn extend_rec(&self, names: Rc<[String]>) -> (Env, Rc<std::cell::RefCell<Vec<Value>>>) {
574+ let cell = Rc::new(std::cell::RefCell::new(Vec::new()));
823− // Collect before allocating: `self` and all values the evaluator is
575+ (
824− // using are still ordinary strong roots during this check.
576+ Env {
825− maybe_collect_cycles();
577+ frame: Some(Rc::new(Frame {
826− let cell = Rc::new(std::cell::RefCell::new(Vec::with_capacity(names.len())));
578+ kind: FrameKind::Rec(cell.clone()),
827− let frame = Rc::new(Frame {
579+ parent: self.clone(),
828− kind: FrameKind::Rec(names, cell.clone()),
580+ })),
829− parent: self.clone(),
581+ },
830− });
582+ cell,
831− RECURSIVE_FRAMES.with(|registry| {
583+ )
832− let mut registry = registry.borrow_mut();
833− registry.frames.push(Rc::downgrade(&frame));
834− registry.allocations += 1;
835− });
836− (Env { frame: Some(frame) }, cell)
837 }
584 }
838
585
839 pub fn lookup(&self, name: &str) -> Option<Value> {
586 pub fn lookup(&self, name: &str) -> Option<Value> {
@@ -847,13 +594,11 @@ impl Env {
847 }
594 }
848 }
595 }
849 }
596 }
850− FrameKind::Rec(names, cell) => {
597+ FrameKind::Rec(cell) => {
851− // the frame binds its names before it holds their values:
598+ for (n, v) in cell.borrow().iter().rev() {
852− // one not evaluated yet is unbound here, not a same-named
599+ if n == name {
853− // definition further out, which config.j's binders may
600+ return Some(v.clone());
854− // reuse (§4.2)
601+ }
855− if let Some(i) = names.iter().rposition(|n| n == name) {
856− return cell.borrow().get(i).cloned();
857 }
602 }
858 }
603 }
859 FrameKind::Big(m) => {
604 FrameKind::Big(m) => {
@@ -875,7 +620,7 @@ impl Env {
875 while let Some(f) = cur {
620 while let Some(f) = cur {
876 match &f.kind {
621 match &f.kind {
877 FrameKind::Small(_) => small_frames += 1,
622 FrameKind::Small(_) => small_frames += 1,
878− FrameKind::Big(_) | FrameKind::Rec(..) => {}
623+ FrameKind::Big(_) | FrameKind::Rec(_) => {}
879 }
624 }
880 cur = f.parent.frame.as_ref();
625 cur = f.parent.frame.as_ref();
881 }
626 }
@@ -891,8 +636,8 @@ impl Env {
891 out.insert(n.clone());
636 out.insert(n.clone());
892 }
637 }
893 }
638 }
894− FrameKind::Rec(names, _) => {
639+ FrameKind::Rec(cell) => {
895− for n in names.iter() {
640+ for (n, _) in cell.borrow().iter() {
896 out.insert(n.clone());
641 out.insert(n.clone());
897 }
642 }
898 }
643 }
@@ -907,302 +652,6 @@ impl Env {
907 }
652 }
908}
653}
909
654
910−// A recursive environment owns its bindings, and a closure in those bindings
911−// may own the environment again. Weak captures would lose an escaping
912−// closure's bindings, so these are real strong references. Collect their
913−// unreachable cycles by subtracting the references inside their owned graph
914−// from each node's reference count. References from outside that graph are
915−// roots: returned values, the interpreter, and the running machine's states
916−// and continuations. No explicit evaluator root stack is needed.
917−//
918−// Only recursive frames can introduce cycles in language values. The
919−// registry is weak, and all traversal and marking below use explicit work
920−// lists, including through shared lists, records, and function wrappers.
921−#[derive(Default)]
922−struct RecursiveFrames {
923− frames: Vec<Weak<Frame>>,
924− allocations: usize,
925− next_collection: usize,
926− without_environments: HashMap<(u8, usize), EnvironmentFree>,
927− cached_since_pruning: usize,
928− collecting: bool,
929−}
930−
931−thread_local! {
932− static RECURSIVE_FRAMES: std::cell::RefCell<RecursiveFrames> =
933− std::cell::RefCell::new(RecursiveFrames::default());
934−}
935−
936−const MIN_CYCLE_BATCH: usize = 256;
937−
938−fn maybe_collect_cycles() {
939− let due = RECURSIVE_FRAMES.with(|registry| {
940− let registry = registry.borrow();
941− !registry.collecting
942− && registry.allocations >= registry.next_collection.max(MIN_CYCLE_BATCH)
943− });
944− if due {
945− collect_cycles();
946− }
947−}
948−
949−/// Reclaim dead recursive environments. The caller must retain any result
950−/// it wants to return while this runs, as an ordinary strong reference.
951−pub(crate) fn collect_cycles() {
952− RECURSIVE_FRAMES.with(|shared| {
953− let mut registry = {
954− let mut registry = shared.borrow_mut();
955− if registry.collecting { return; }
956− let previous = std::mem::take(&mut *registry);
957− registry.collecting = true;
958− previous
959− };
960− // Do not hold the registry borrow while dropping values. Opaque
961− // Rust backend captures can run destructors that allocate frames or
962− // drop another interpreter; such nested collections are deferred.
963− let mut graph = CycleGraph::default();
964− for frame in &registry.frames {
965− if let Some(frame) = frame.upgrade() {
966− graph.add(CycleNode::Frame(frame));
967− }
968− }
969− graph.trace(&registry.without_environments);
970− registry.cached_since_pruning += graph.cache_environment_free(&mut registry.without_environments);
971− graph.collect();
972− // Release the collector's temporary roots before pruning the weak
973− // registry. Live recursive frames set the next batch size so a
974− // growing set of escaping closures costs amortized linear work.
975− drop(graph);
976− registry.frames.retain(|frame| frame.strong_count() != 0);
977− registry.allocations = 0;
978− registry.next_collection = registry.frames.len().max(MIN_CYCLE_BATCH);
979− // Immutable repository trees usually hold no functions. Remember
980− // that once, without keeping their values alive. Pruning is batched
981− // too: a large, live tree must not be scanned on every collection.
982− if registry.frames.is_empty() {
983− registry.without_environments.clear();
984− registry.cached_since_pruning = 0;
985− } else if registry.cached_since_pruning >=
986− (registry.without_environments.len() / 2).max(MIN_CYCLE_BATCH)
987− {
988− registry.without_environments.retain(|_, cached| cached.is_alive());
989− registry.cached_since_pruning = 0;
990− }
991− let mut pending = shared.borrow_mut();
992− registry.frames.append(&mut pending.frames);
993− registry.allocations += pending.allocations;
994− *pending = registry;
995− });
996−}
997−
998−// These containers are immutable while shared. Weak references both avoid
999−// extending their lives and prevent address reuse from giving a false hit.
1000−// Rc::make_mut dissociates weak pointers before it can change a container.
1001−enum EnvironmentFree {
1002− List(Weak<Vec<Value>>),
1003− Record(Weak<RecordMap>),
1004−}
1005−
1006−impl EnvironmentFree {
1007− fn is_alive(&self) -> bool {
1008− match self {
1009− Self::List(v) => v.strong_count() != 0,
1010− Self::Record(v) => v.strong_count() != 0,
1011− }
1012− }
1013−}
1014−
1015−enum CycleNode {
1016− Frame(Rc<Frame>),
1017− Bindings(Rc<std::cell::RefCell<Vec<Value>>>),
1018− Function(Rc<FunVal>),
1019− List(Rc<Vec<Value>>),
1020− Record(Rc<RecordMap>),
1021−}
1022−
1023−impl CycleNode {
1024− fn key(&self) -> (u8, usize) {
1025− match self {
1026− Self::Frame(v) => (0, Rc::as_ptr(v) as usize),
1027− Self::Bindings(v) => (1, Rc::as_ptr(v) as usize),
1028− Self::Function(v) => (2, Rc::as_ptr(v) as usize),
1029− Self::List(v) => (3, Rc::as_ptr(v) as usize),
1030− Self::Record(v) => (4, Rc::as_ptr(v) as usize),
1031− }
1032− }
1033−
1034− fn strong_count(&self) -> usize {
1035− match self {
1036− Self::Frame(v) => Rc::strong_count(v),
1037− Self::Bindings(v) => Rc::strong_count(v),
1038− Self::Function(v) => Rc::strong_count(v),
1039− Self::List(v) => Rc::strong_count(v),
1040− Self::Record(v) => Rc::strong_count(v),
1041− }
1042− }
1043−
1044− fn children(&self) -> Vec<CycleNode> {
1045− let mut out = Vec::new();
1046− let mut value = |v: &Value| match v {
1047− Value::Fun(f) => out.push(Self::Function(f.clone())),
1048− // A list view owns its whole backing vector, including the
1049− // prefix that `tail`/`drop` no longer exposes.
1050− Value::List(xs) => out.push(Self::List(xs.items.clone())),
1051− Value::Record(fields) => out.push(Self::Record(fields.clone())),
1052− // Backend thunks are opaque Rust closures. Their references,
1053− // if any, remain external roots; never guess what they own.
1054− _ => {}
1055− };
1056− match self {
1057− Self::Frame(frame) => {
1058− match &frame.kind {
1059− FrameKind::Small(bindings) => {
1060− for (_, v) in bindings { value(v); }
1061− }
1062− FrameKind::Big(bindings) => {
1063− for v in bindings.values() { value(v); }
1064− }
1065− FrameKind::Rec(_, bindings) => {
1066− out.push(Self::Bindings(bindings.clone()));
1067− }
1068− }
1069− if let Some(parent) = &frame.parent.frame {
1070− out.push(Self::Frame(parent.clone()));
1071− }
1072− }
1073− Self::Bindings(bindings) => {
1074− for v in bindings.borrow().iter() { value(v); }
1075− }
1076− Self::Function(f) => {
1077− match f.as_ref() {
1078− FunVal::Closure { env, applied_args, .. } => {
1079− for v in applied_args { value(v); }
1080− if let Some(frame) = &env.frame {
1081− out.push(Self::Frame(frame.clone()));
1082− }
1083− }
1084− FunVal::Builtin { args, .. } => {
1085− for v in args { value(v); }
1086− }
1087− FunVal::OrFun(a, b) | FunVal::ComposeLazy(a, b) => {
1088− value(a);
1089− value(b);
1090− }
1091− FunVal::Labelled(_, f) => value(f),
1092− }
1093− }
1094− Self::List(items) => {
1095− for v in items.iter() { value(v); }
1096− }
1097− Self::Record(fields) => {
1098− for v in fields.values() { value(v); }
1099− }
1100− }
1101− out
1102− }
1103−}
1104−
1105−#[derive(Default)]
1106−struct CycleGraph {
1107− // Exactly one temporary strong reference per node. All temporary edge
1108− // clones are consumed by `add` before reference counts are examined.
1109− nodes: Vec<CycleNode>,
1110− indices: HashMap<(u8, usize), usize>,
1111− edges: Vec<Vec<usize>>,
1112− incoming: Vec<usize>,
1113−}
1114−
1115−impl CycleGraph {
1116− fn add(&mut self, node: CycleNode) -> usize {
1117− let key = node.key();
1118− if let Some(&index) = self.indices.get(&key) {
1119− return index;
1120− }
1121− let index = self.nodes.len();
1122− self.indices.insert(key, index);
1123− self.nodes.push(node);
1124− self.edges.push(Vec::new());
1125− self.incoming.push(0);
1126− index
1127− }
1128−
1129− fn trace(&mut self, without_environments: &HashMap<(u8, usize), EnvironmentFree>) {
1130− let mut index = 0;
1131− while index < self.nodes.len() {
1132− for child in self.nodes[index].children() {
1133− if without_environments.contains_key(&child.key()) {
1134− continue;
1135− }
1136− let next = self.add(child);
1137− self.edges[index].push(next);
1138− self.incoming[next] += 1;
1139− }
1140− index += 1;
1141− }
1142− }
1143−
1144− fn cache_environment_free(&self, cache: &mut HashMap<(u8, usize), EnvironmentFree>) -> usize {
1145− let mut parents = vec![Vec::new(); self.nodes.len()];
1146− for (index, edges) in self.edges.iter().enumerate() {
1147− for &next in edges { parents[next].push(index); }
1148− }
1149− let mut reaches_frame = vec![false; self.nodes.len()];
1150− let mut work = Vec::new();
1151− for (index, node) in self.nodes.iter().enumerate() {
1152− if matches!(node, CycleNode::Frame(_)) {
1153− reaches_frame[index] = true;
1154− work.push(index);
1155− }
1156− }
1157− while let Some(index) = work.pop() {
1158− for &parent in &parents[index] {
1159− if !reaches_frame[parent] {
1160− reaches_frame[parent] = true;
1161− work.push(parent);
1162− }
1163− }
1164− }
1165− let mut added = 0;
1166− for (index, node) in self.nodes.iter().enumerate() {
1167− if !reaches_frame[index] {
1168− let cached = match node {
1169− CycleNode::List(v) => EnvironmentFree::List(Rc::downgrade(v)),
1170− CycleNode::Record(v) => EnvironmentFree::Record(Rc::downgrade(v)),
1171− _ => continue,
1172− };
1173− if cache.insert(node.key(), cached).is_none() { added += 1; }
1174− }
1175− }
1176− added
1177− }
1178−
1179− fn collect(&self) {
1180− let mut reachable = vec![false; self.nodes.len()];
1181− let mut work = Vec::new();
1182− for (index, node) in self.nodes.iter().enumerate() {
1183− if node.strong_count() > self.incoming[index] + 1 {
1184− reachable[index] = true;
1185− work.push(index);
1186− }
1187− }
1188− while let Some(index) = work.pop() {
1189− for &next in &self.edges[index] {
1190− if !reachable[next] {
1191− reachable[next] = true;
1192− work.push(next);
1193− }
1194− }
1195− }
1196− for (index, node) in self.nodes.iter().enumerate() {
1197− if !reachable[index] {
1198− if let CycleNode::Bindings(bindings) = node {
1199− bindings.borrow_mut().clear();
1200− }
1201− }
1202− }
1203− }
1204−}
1205−
1206// ----------------------------------------------------------------------
655// ----------------------------------------------------------------------
1207// structural equality (§4.5); comparing functions is a crash
656// structural equality (§4.5); comparing functions is a crash
1208// ----------------------------------------------------------------------
657// ----------------------------------------------------------------------
tests/base.rs+2−9
@@ -211,12 +211,6 @@ fn goto_variants() {
211 assert_eq!(focus_msg(&v), "first");
211 assert_eq!(focus_msg(&v), "first");
212 let m = crash(&mut i, &cfg, "goto (labelled \"nope\")", repo.clone());
212 let m = crash(&mut i, &cfg, "goto (labelled \"nope\")", repo.clone());
213 assert!(m.contains("expected one revision, got 0"), "{}", m);
213 assert!(m.contains("expected one revision, got 0"), "{}", m);
214− // a lifted `or` over the curried goto still guards the repo argument
215− // (§4.6): goto applied to the revset is a function, so the lift goes on
216− let v = ok(&mut i, &cfg, "(goto or (\\_ -> id)) %nope", repo.clone());
217− assert_eq!(focus_id(&v), focus_id(&repo));
218− let v = ok(&mut i, &cfg, "(goto or (\\_ -> id)) %feat", repo.clone());
219− assert_eq!(focus_msg(&v), "first");
220 let m = crash(&mut i, &cfg, "into @kqqqqqqq", repo.clone());
214 let m = crash(&mut i, &cfg, "into @kqqqqqqq", repo.clone());
221 assert!(m.contains("not a child"), "{}", m);
215 assert!(m.contains("not a child"), "{}", m);
222 // child by predicate: move to the parent, then to the matching child
216 // child by predicate: move to the parent, then to the matching child
@@ -413,11 +407,10 @@ fn inspection_definitions() {
413 // meta of minted crashes
407 // meta of minted crashes
414 let m = crash(&mut i, &cfg, "meta @", repo.clone());
408 let m = crash(&mut i, &cfg, "meta @", repo.clone());
415 assert!(m.contains("stored commit") || m.contains("meta"), "{}", m);
409 assert!(m.contains("stored commit") || m.contains("meta"), "{}", m);
416− // review renders changed paths with difft or the built-in fallback;
410+ // review with no difft falls to crash (difft not on PATH in tests)
417− // "no changes" is for a focus that changes nothing.
418 let r = ev(&mut i, &cfg, "review", repo.clone());
411 let r = ev(&mut i, &cfg, "review", repo.clone());
419 match r {
412 match r {
420− Ok(v) => assert_ne!(v.as_text().unwrap(), "no changes\n"),
413+ Ok(v) => assert_eq!(v.as_text().unwrap(), "no changes\n"),
421 Err(m) => assert!(m.contains("difft"), "{}", m),
414 Err(m) => assert!(m.contains("difft"), "{}", m),
422 }
415 }
423}
416}
tests/builtins.rs+6−45
@@ -36,7 +36,7 @@ fn ok(i: &mut Interp, cfg: &config::Config, src: &str) -> Value {
36
36
37fn crash(i: &mut Interp, cfg: &config::Config, src: &str) -> String {
37fn crash(i: &mut Interp, cfg: &config::Config, src: &str) -> String {
38 match ev(i, cfg, src) {
38 match ev(i, cfg, src) {
39− Ok(v) => panic!("{} unexpectedly succeeded: {}", src, j::show::show(i, &v).unwrap()),
39+ Ok(v) => panic!("{} unexpectedly succeeded: {}", src, j::show::show(i, &v)),
40 Err(m) => m,
40 Err(m) => m,
41 }
41 }
42}
42}
@@ -48,8 +48,8 @@ macro_rules! check {
48 value_eq(&got, &$want).unwrap_or(false),
48 value_eq(&got, &$want).unwrap_or(false),
49 "{} => {}, want {}",
49 "{} => {}, want {}",
50 $src,
50 $src,
51− j::show::show(&$i, &got).unwrap(),
51+ j::show::show(&$i, &got),
52− j::show::show(&$i, &$want).unwrap()
52+ j::show::show(&$i, &$want)
53 );
53 );
54 }};
54 }};
55}
55}
@@ -81,32 +81,6 @@ fn arithmetic() {
81 check!(i, cfg, "3 >= 4", Value::Bool(false));
81 check!(i, cfg, "3 >= 4", Value::Bool(false));
82}
82}
83
83
84−#[test]
85−fn blob_hex_decodes_exact_bytes_and_file_type() {
86− use j::value::BlobKind;
87− let (mut i, cfg) = make_interp();
88− for (name, kind) in [
89− ("regular", BlobKind::Regular),
90− ("executable", BlobKind::Executable),
91− ("symlink", BlobKind::Symlink),
92− ] {
93− for (hex, bytes) in [("", vec![]), ("00ff80A5", vec![0, 255, 128, 165])] {
94− let v = ok(&mut i, &cfg, &format!("blobHex \"{}\" \"{}\"", name, hex));
95− let Value::Blob(b) = v else { panic!("expected Blob") };
96− assert_eq!(b.kind, kind);
97− assert_eq!(b.bytes().unwrap(), bytes);
98− assert!(!b.is_unresolved());
99− }
100− }
101− for hex in ["f", "xyz", "0g", "é", "00 ff"] {
102− let msg = crash(&mut i, &cfg, &format!("blobHex \"regular\" \"{}\"", hex));
103− assert!(msg.contains("invalid hexadecimal content"), "{}", msg);
104− }
105− assert!(crash(&mut i, &cfg, "blobHex \"directory\" \"\"").contains("kind must be"));
106− assert!(crash(&mut i, &cfg, "blobHex 1 \"00\"").contains("expected Text"));
107− assert!(crash(&mut i, &cfg, "blobHex \"regular\" 1").contains("expected Text"));
108−}
109−
110#[test]
84#[test]
111fn identity_and_const() {
85fn identity_and_const() {
112 let (mut i, cfg) = make_interp();
86 let (mut i, cfg) = make_interp();
@@ -234,22 +208,9 @@ fn show_renders_literals() {
234 check!(i, cfg, "show (++)", Value::text("(++)"));
208 check!(i, cfg, "show (++)", Value::text("(++)"));
235 check!(i, cfg, "show Commit", Value::text("Commit"));
209 check!(i, cfg, "show Commit", Value::text("Commit"));
236 check!(i, cfg, "show id", Value::text("id"));
210 check!(i, cfg, "show id", Value::text("id"));
237− // lambda shows its source, exactly as written (§5.2)
211+ // lambda shows its source
238− check!(i, cfg, "show (\\x -> x + 1)", Value::text("\\x -> x + 1"));
212+ let v = ok(&mut i, &cfg, "show (\\x -> x + 1)");
239−}
213+ assert!(v.as_text().unwrap().contains("x"), "{}", v.as_text().unwrap());
240−
241−#[test]
242−fn show_renders_selectors() {
243− // a selector is the SELECTOR atom `.name` (§3.4), not the accessor
244− // applied to the field name it carries internally (§5.2)
245− let (mut i, cfg) = make_interp();
246− check!(i, cfg, "show (.id)", Value::text(".id"));
247− check!(i, cfg, "show [(.id)]", Value::text("[(.id)]"));
248− check!(i, cfg, "show (map (.id))", Value::text("map (.id)"));
249− // and the rendering evaluates back to the same selector
250− let shown = ok(&mut i, &cfg, "show (map (.id))");
251− let src = format!("({}) [{{ id = 5 }}]", shown.as_text().unwrap());
252− check!(i, cfg, &src, ints(&[5]));
253}
214}
254
215
255#[test]
216#[test]
tests/cli.rs+78−4335

This diff is too big to show here.

tests/core.rs+1−1

This diff is too big to show here.

tests/difft.rsdeleted−74

This diff is too big to show here.

tests/display.rs+5−925

This diff is too big to show here.

tests/domain.rs+1−111

This diff is too big to show here.

tests/env_lifetimes.rsdeleted−215

This diff is too big to show here.

tests/eval.rs+9−1072

This diff is too big to show here.

tests/help.rsdeleted−326

This diff is too big to show here.

tests/interop.rs+10−173

This diff is too big to show here.

tests/laws.proptest-regressions−1

This diff is too big to show here.

tests/laws.rs+6−237

This diff is too big to show here.

tests/lex.rs−12

This diff is too big to show here.

tests/more.rs+1−322

This diff is too big to show here.

tests/onboarding.rsdeleted−200

This diff is too big to show here.

tests/onboarding_vocab.rsdeleted−153

This diff is too big to show here.

tests/parse.rs−314

This diff is too big to show here.

tests/performance_lookup.rsdeleted−255

This diff is too big to show here.

tests/remote.rs+2−2336

This diff is too big to show here.

tests/review_fallback.rsdeleted−110

This diff is too big to show here.

tests/setup.rsdeleted−144

This diff is too big to show here.

tests/show.rs+25−567

This diff is too big to show here.

tests/showprop.proptest-regressions−1

This diff is too big to show here.

tests/showprop.rs+5−130

This diff is too big to show here.

tests/terminal.rsdeleted−621

This diff is too big to show here.

tests/tree.rs+97−1116

This diff is too big to show here.

tests/ux_cli.rsdeleted−105

This diff is too big to show here.

tests/validation.rs+8−816

This diff is too big to show here.