Skip to content

feat(config): default writes to user scope; import a config file (0.23.0) - #184

Merged
johnyaku merged 1 commit into
mainfrom
feat/config-user-scope-default
Aug 20, 2026
Merged

johnyaku merged 1 commit into
mainfrom
feat/config-user-scope-default

Conversation

@johnyaku

Copy link
Copy Markdown
Contributor

Why

dt config set wrote project scope, which surprised people twice over: a setting made in one repo did not apply in the next one, and .dt/config.yaml is tracked by git, so a personal preference arrived in every collaborator's checkout on the next pull.

Most settings — a GitHub owner, an SSH username, a cache root, a staging dir — describe the person and the machine rather than the repository. User scope is the one that does what was meant; project scope is the sharper instrument and now has to be asked for with --project.

What changed

The default. set, add, remove, and unset all write user scope unless a scope flag says otherwise. A bare dt config set therefore no longer writes a git-tracked file.

Two new diagnostics, because flipping the default creates two new ways to be quietly wrong — cases where the command succeeds and the effective value isn't what you asked for:

  • A user-scope write under a project or local value changes nothing observable, which reads as the tool ignoring the command. It now names the file that outranks it, the value still in force, and the scope to target.
  • unset misses by default whenever the key is project-scoped. Instead of a bare "not found":
Error: Key 'cache.root' is not set in user configuration, but is set in project/system:
  --project	/path/to/repo/.dt/config.yaml
  --system	/g/data/a56/config/xdg/dt/config.yaml
Re-run with the scope you meant, e.g. 'dt config unset --project cache.root'.

Both are backed by a new cfg.scopes_defining(key). set also now prints the path it wrote, since the default location is off in ~ and no longer visible from where you're standing.

dt config import <file>, for the case the default change doesn't cover: an offsite collaborator on a different filesystem, who needs the team's registry and secret-manager settings but owns their own paths.

$ dt config import lab-defaults.yaml
lab-defaults.yaml -> user config (/home/alice/.config/dt/config.yaml)
  + secrets.backend      gcp
  + secrets.gcp.project  swarbrick-secrets
  ~ owner                mine -> swarbricklab

Replace 1 existing value in user config? [y/N]: y
Wrote 3 settings to user config (/home/alice/.config/dt/config.yaml).

The file is an ordinary config.yaml — the same one dt config path --user points at — so it can be emailed and imported without knowing where config lives on that machine, and copying it into place by hand still works. Over cp it:

  • merges — keys the file omits are left alone, so lab defaults don't wipe the collaborator's local paths;
  • diffs and confirms replacements (additions apply straight away; --yes, --dry-run);
  • validates — a list under a key that takes a scalar is refused up front, rather than breaking released versions later (the failure mode documented in LIST_VALUED_KEYS);
  • flags imported paths that don't exist here — the usual symptom of a file from another filesystem.

Re-importing is a no-op, so an updated copy is safe to send. Scope flags work as on set.

Incidental: set_value now delegates to a new bulk set_values (unchanged from outside). The large-files hook hint gains --project — under the new default it would otherwise raise the limit in every repo, not the one with the big files.

Deliberately not included

URL fetching and placeholder substitution. The narrowed case is handing over a file; with paths simply omitted from the shared file there is nothing to template, and URL support brings network/TLS trust surface for no gain here. For a team on one filesystem, a shared system-scope file via XDG_CONFIG_DIRS is still the better answer than handing out copies — the docs say so, so import doesn't become the default answer to the wrong question.

Test notes

The config integration tests ran dt in a subprocess inheriting os.environ, so under the new default they would have rewritten the developer's own ~/.config/dt/config.yaml. They now pin XDG_CONFIG_HOME/XDG_CONFIG_DIRS to tmp_path, and the three bare config set calls in test_clone_cmd.py pass --project.

Default-scope behaviour was previously untested — every test passed an explicit flag — so it's now covered at both levels, along with scopes_defining, the shadow warning, the unset-miss message, and import.

2331 passed, 2 skipped (tests/unit + tests/integration/test_config_cmd.py), run on a compute node via qxub.

Breaking change

Anyone with muscle memory for dt config set <key> <value> writing .dt/config.yaml needs --project now. Existing config files are untouched and precedence is unchanged, so nothing already committed changes behaviour. Minor version bump to 0.23.0.

🤖 Generated with Claude Code

…3.0)

`dt config set` wrote project scope, which surprised people twice over: a
setting made in one repo did not apply in the next one, and `.dt/config.yaml`
is tracked by git, so a personal preference arrived in every collaborator's
checkout on the next pull. Most settings -- a GitHub owner, an SSH username, a
cache root, a staging dir -- describe the person and the machine rather than
the repository, so user scope is the one that does what was meant. Project
scope is the sharper instrument and now has to be asked for by name.

Flipping the default creates two new ways to be quietly wrong, so both now
speak up. A user-scope write under a project or local value succeeds while
changing nothing observable, which reads as the tool ignoring the command: it
now says which file outranks it, what value is still in force, and which scope
to target. And `unset`, which misses by default whenever the key is
project-scoped, now lists the scopes that do hold the key with their paths
instead of a bare "not found".

Also adds `dt config import <file>`, for the case the default change does not
cover: an offsite collaborator on a different filesystem, who needs the team's
registry and secret-manager settings but owns their own paths. The file is an
ordinary config.yaml -- the same one `dt config path --user` points at -- so it
can be handed over and imported without knowing where config lives on that
machine, and copying it into place by hand still works. Over `cp` it merges
rather than replaces (keys the file omits are left alone, so lab defaults do
not wipe local paths), diffs and confirms replacements, refuses a list under a
key that takes a scalar before it can break a released version, and flags
imported paths that do not exist here -- the usual symptom of a file from
another filesystem.

`set_value` now delegates to a new bulk `set_values`, unchanged from outside.
The large-files hook hint gains `--project`: under the new default it would
otherwise raise the limit in every repo, not the one with the big files.

The config integration tests ran `dt` in a subprocess inheriting os.environ, so
with the new default they would have rewritten the developer's own
~/.config/dt/config.yaml; they now pin XDG_CONFIG_HOME to tmp_path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@johnyaku
johnyaku merged commit de3acd2 into main Aug 20, 2026
1 check passed
@johnyaku
johnyaku deleted the feat/config-user-scope-default branch August 20, 2026 02:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant