Skip to content

Commit

Permalink
Add a "config" command with "get" and "list" subcommands
Browse files Browse the repository at this point in the history
Partially fixes jj-vcs#531.
  • Loading branch information
dbarnett committed Dec 27, 2022
1 parent d89619c commit cc0f642
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 2 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* `jj git push` now accepts multiple `--branch`/`--change` arguments

* `jj config list` command prints values from config (with other subcommands
coming soon).

### Fixed bugs

* When sharing the working copy with a Git repo, we used to forget to export
Expand All @@ -39,6 +42,7 @@ Thanks to the people who made this release happen!
* Danny Hooper ([email protected])
* Yuya Nishihara (@yuja)
* Ilya Grigoriev (@ilyagr)
* David Barnett (@dbarnett)

## [0.6.1] - 2022-12-05

Expand Down
43 changes: 43 additions & 0 deletions src/cli_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1234,6 +1234,49 @@ pub fn write_commit_summary(
Ok(())
}

pub fn write_config_entry(
ui: &mut Ui,
path: &str,
value: config::Value,
) -> Result<(), CommandError> {
match value.kind {
// Handle table values specially to render each child nicely on its own line.
config::ValueKind::Table(table) => {
// TODO: Remove sorting when config crate maintains deterministic ordering.
for (key, table_val) in table.into_iter().sorted_by_key(|(k, _)| k.to_owned()) {
let key_path = match path {
"" => key,
_ => format!("{path}.{key}"),
};
write_config_entry(ui, key_path.as_str(), table_val)?;
}
}
_ => writeln!(ui, "{path}={}", serialize_config_value(value))?,
};
Ok(())
}

// TODO: Use a proper TOML library to serialize instead.
fn serialize_config_value(value: config::Value) -> String {
match value.kind {
config::ValueKind::Table(table) => format!(
"{{{}}}",
// TODO: Remove sorting when config crate maintains deterministic ordering.
table
.into_iter()
.sorted_by_key(|(k, _)| k.to_owned())
.map(|(k, v)| format!("{k}={}", serialize_config_value(v)))
.join(", ")
),
config::ValueKind::Array(vals) => format!(
"[{}]",
vals.into_iter().map(serialize_config_value).join(", ")
),
config::ValueKind::String(val) => format!("{val:?}"),
_ => value.to_string(),
}
}

pub fn short_commit_description(commit: &Commit) -> String {
let first_line = commit.description().split('\n').next().unwrap();
format!("{} ({})", short_commit_hash(commit.id()), first_line)
Expand Down
62 changes: 60 additions & 2 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use std::{fs, io};
use chrono::{FixedOffset, LocalResult, TimeZone, Utc};
use clap::builder::NonEmptyStringValueParser;
use clap::{ArgGroup, ArgMatches, CommandFactory, FromArgMatches, Subcommand};
use config::Source;
use itertools::Itertools;
use jujutsu_lib::backend::{CommitId, Timestamp, TreeValue};
use jujutsu_lib::commit::Commit;
Expand Down Expand Up @@ -53,8 +54,8 @@ use pest::Parser;
use crate::cli_util::{
self, check_stale_working_copy, print_checkout_stats, print_failed_git_export,
resolve_base_revs, short_commit_description, short_commit_hash, user_error,
user_error_with_hint, write_commit_summary, Args, CommandError, CommandHelper, DescriptionArg,
RevisionArg, WorkspaceCommandHelper,
user_error_with_hint, write_commit_summary, write_config_entry, Args, CommandError,
CommandHelper, DescriptionArg, RevisionArg, WorkspaceCommandHelper,
};
use crate::config::FullCommandArgs;
use crate::diff_util::{self, DiffFormat, DiffFormatArgs};
Expand All @@ -69,6 +70,8 @@ use crate::ui::Ui;
enum Commands {
Version(VersionArgs),
Init(InitArgs),
#[command(subcommand)]
Config(ConfigSubcommand),
Checkout(CheckoutArgs),
Untrack(UntrackArgs),
Files(FilesArgs),
Expand Down Expand Up @@ -142,6 +145,31 @@ struct InitArgs {
git_repo: Option<String>,
}

/// Get config options
///
/// Operates on jj configuration, which comes from the config file and
/// environment variables. Uses the config file at ~/.jjconfig.toml or
/// $XDG_CONFIG_HOME/jj/config.toml, unless overridden with the JJ_CONFIG
/// environment variable.
///
/// For supported config options and more details about jj config, see
/// https://github.com/martinvonz/jj/blob/main/docs/config.md.
///
/// Note: Currently only supports getting config options, but support for
/// setting options and editing config files is also planned (see
/// https://github.com/martinvonz/jj/issues/531).
#[derive(clap::Subcommand, Clone, Debug)]
enum ConfigSubcommand {
/// List variables set in config file, along with their values.
#[command(visible_alias("l"))]
List {
/// An optional name of a specific config option to look up.
#[arg(value_parser=NonEmptyStringValueParser::new())]
name: Option<String>,
// TODO: Support --show-origin once mehcode/config-rs#319 is done.
},
}

/// Create a new, empty change and edit it in the working copy
///
/// For more information, see
Expand Down Expand Up @@ -1172,6 +1200,35 @@ Set `ui.allow-init-native` to allow initializing a repo with the native backend.
Ok(())
}

fn cmd_config(
ui: &mut Ui,
_command: &CommandHelper,
subcommand: &ConfigSubcommand,
) -> Result<(), CommandError> {
ui.request_pager();
match subcommand {
ConfigSubcommand::List { name } => {
let raw_values = match name {
Some(name) => {
ui.settings()
.config()
.get::<config::Value>(name)
.map_err(|e| match e {
config::ConfigError::NotFound { .. } => {
user_error("key not found in config")
}
_ => e.into(),
})?
}
None => ui.settings().config().collect()?.into(),
};
write_config_entry(ui, name.as_deref().unwrap_or(""), raw_values)?;
}
}

Ok(())
}

fn cmd_checkout(
ui: &mut Ui,
command: &CommandHelper,
Expand Down Expand Up @@ -4274,6 +4331,7 @@ pub fn run_command(
match &derived_subcommands {
Commands::Version(sub_args) => cmd_version(ui, command_helper, sub_args),
Commands::Init(sub_args) => cmd_init(ui, command_helper, sub_args),
Commands::Config(sub_args) => cmd_config(ui, command_helper, sub_args),
Commands::Checkout(sub_args) => cmd_checkout(ui, command_helper, sub_args),
Commands::Untrack(sub_args) => cmd_untrack(ui, command_helper, sub_args),
Commands::Files(sub_args) => cmd_files(ui, command_helper, sub_args),
Expand Down
128 changes: 128 additions & 0 deletions tests/test_config_command.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright 2022 The Jujutsu Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use itertools::Itertools;
use regex::Regex;

use crate::common::TestEnvironment;

pub mod common;

#[test]
fn test_config_list_single() {
let test_env = TestEnvironment::default();
test_env.add_config(
r###"
[test-table]
somekey = "some value"
"###
.as_bytes(),
);

let stdout = test_env.jj_cmd_success(
test_env.env_root(),
&["config", "list", "test-table.somekey"],
);
insta::assert_snapshot!(stdout, @r###"
test-table.somekey="some value"
"###);
}

#[test]
fn test_config_list_table() {
let test_env = TestEnvironment::default();
test_env.add_config(
r###"
[test-table]
x = true
y.foo = "abc"
y.bar = 123
"###
.as_bytes(),
);
let stdout = test_env.jj_cmd_success(test_env.env_root(), &["config", "list", "test-table"]);
insta::assert_snapshot!(
stdout,
@r###"
test-table.x=true
test-table.y.bar=123
test-table.y.foo="abc"
"###);
}

#[test]
fn test_config_list_array() {
let test_env = TestEnvironment::default();
test_env.add_config(
r###"
test-array = [1, "b", 3.4]
"###
.as_bytes(),
);
let stdout = test_env.jj_cmd_success(test_env.env_root(), &["config", "list", "test-array"]);
insta::assert_snapshot!(stdout, @r###"
test-array=[1, "b", 3.4]
"###);
}

#[test]
fn test_config_list_inline_table() {
let test_env = TestEnvironment::default();
test_env.add_config(
r###"
[[test-table]]
x = 1
[[test-table]]
y = ["z"]
"###
.as_bytes(),
);
let stdout = test_env.jj_cmd_success(test_env.env_root(), &["config", "list", "test-table"]);
insta::assert_snapshot!(stdout, @r###"
test-table=[{x=1}, {y=["z"]}]
"###);
}

#[test]
fn test_config_list_all() {
let test_env = TestEnvironment::default();
test_env.add_config(
r###"
test-val = [1, 2, 3]
[test-table]
x = true
y.foo = "abc"
y.bar = 123
"###
.as_bytes(),
);
let stdout = test_env.jj_cmd_success(test_env.env_root(), &["config", "list"]);
insta::assert_snapshot!(
find_stdout_lines(r"(test-val|test-table\b[^=]*)", &stdout),
@r###"
test-table.x=true
test-table.y.bar=123
test-table.y.foo="abc"
test-val=[1, 2, 3]
"###);
}

fn find_stdout_lines<'a>(keyname_pattern: &str, stdout: &str) -> String {
let key_line_re = Regex::new(&format!(r"(?m)^{}=.*$", keyname_pattern)).unwrap();
key_line_re
.find_iter(stdout)
.map(|m| m.as_str())
.collect_vec()
.join("\n")
}

0 comments on commit cc0f642

Please sign in to comment.