jj / How jj config unset works

How jj config unset works

jj config unset --user|--repo|--workspace|--file PATH NAME removes one value from one config file and writes the file back.

On this page
  1. 1. Command-line surface
  2. 2. Configuration
  3. 3. End-to-end flow
  4. 4. Core architecture touchpoints
  5. 5. What it reads, and snapshotting
  6. 6. Algorithm and invariants
  7. Finding the value
  8. Removing it
  9. Nothing there
  10. 7. Errors and edge cases
  11. 8. Example output

In short: it picks exactly one file the same way config set does, deletes the value at that name from the file's TOML document, and saves. It fails without writing if the name is not set in that file, or if it names a whole [table]. It never loads the repo and never snapshots.

The command is cmd_config_unset. Its help text: "Update a config file to unset the given option." (source). It is the Unset variant of ConfigCommand, with the visible alias jj config u (source, dispatch).

1. Command-line surface

ArgumentMeaning
NAME (required)A dotted config name, parsed as a TOML key (source).
--user, --repo, --workspace, --file PATHWhich file to edit; exactly one is required. These are the same ConfigTargetArgs that config set and config edit use (source, definition).

Shell completion for NAME runs jj config list and offers only names whose source is user, repo or workspace config, each tagged with its level and value. If one level flag is already on the command line, it lists that level only, including values overridden by higher layers (source).

The global flags are on the CLI page.

2. Configuration

The loaded config decides which file is edited, exactly as for config set: the first loaded file of the chosen level, or that level's default path if none was loaded (source), or a --file path checked by resolve_file_to_edit. It only touches that file, so a value also set in another layer still applies afterwards.

3. End-to-end flow

jj config unset: call flow clap parses the name and one target flag. edit_config_file picks one file and loads it, or starts an empty document if it does not exist. delete_value walks the parent keys; a missing or non-table parent means nothing to delete. It then checks the leaf: a missing leaf also means nothing to delete, and the command fails with "NAME doesn't exist". A non-inline table fails with "Would delete entire table". A value or inline table is removed, and the whole file is written back. clap: NAME and one target --user | --repo | --workspace | --file edit_config_file: pick one file missing file → empty document delete_value: walk the parents missing or non-table parent → None nothing deleted Error: "NAME" doesn't exist, exit 1 check the leaf vacant → None; value → remove leaf is a [table] Would delete entire table NAME ConfigFile::save temp file renamed over the file

4. Core architecture touchpoints

PieceRole here
ConfigTargetArgs::edit_config_fileShared with config set and config edit: turns the target flag into one ConfigFile, or fails with "No user/repo/workspace config path found to edit".
ConfigFile::delete_valueCopies the layer if it is shared, then calls ConfigLayer::delete_value, which returns the old value, or none if nothing was there.
ConfigUpdateErrorIts WouldDeleteTable variant is the only way the deletion itself fails.
ConfigFile::saveSerializes the edited document and writes it to the file's path.

5. What it reads, and snapshotting

It reads only the chosen config file, through the config already loaded at startup or from disk, and never the repo or the working copy, so there is no snapshot. --repo and --workspace look up the repo's or workspace's config path as config set does (source). It writes only when a value was actually removed (source).

6. Algorithm and invariants

Finding the value

ConfigLayer::delete_value splits off the last key and walks the others from the document root. If a parent is missing, or is not a table or inline table, the result is "nothing deleted" rather than an error (source). So a key inside an inline table can be removed, leaving inline-table = {} (test).

Removing it

If the leaf is a value, including an array or an inline table, it is removed and its old value returned. If it is a non-inline [table], the call fails with WouldDeleteTable; the file is not written (source, test). A table emptied by removing its last key stays as an empty table header (test). Text that belonged to the removed key goes with it: in upstream's tests the #:schema line that config set wrote into a new repo config file is gone once its only key is unset, leaving the file empty (test).

Nothing there

If no value was removed, the command fails with "NAME" doesn't exist before saving (source). This includes a target file that does not exist yet, which edit_config_file loads as an empty document, so unset never creates a config file.

7. Errors and edge cases

SituationResult
Name not set in that fileError: "NAME" doesn't exist, exit 1 (test).
Name is a non-inline tableError: Failed to unset NAME / Caused by: Would delete entire table NAME, exit 1, file unchanged (test).
Name is an inline tableRemoved like any value (test).
No target or no nameA clap usage error, exit 2: the target group is required (source).

8. Example output

From upstream's tests, with a user config file holding inline-table = { foo = true } and a [non-inline-table] with foo = true (source, last command):

$ jj config unset --user inline-table
$ jj config unset --user non-inline-table
Error: Failed to unset non-inline-table
Caused by: Would delete entire table non-inline-table
$ cat ~/.config/jj/config.toml
[non-inline-table]
foo = true
$ jj config unset --user nonexistent
Error: "nonexistent" doesn't exist