just playing with tangled
1// Copyright 2020 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::io::Write;
16
17use clap::builder::NonEmptyStringValueParser;
18use itertools::Itertools;
19use tracing::instrument;
20
21use crate::cli_util::{
22 get_new_config_file_path, run_ui_editor, serialize_config_value, write_config_value_to_file,
23 CommandHelper,
24};
25use crate::command_error::{config_error, user_error, CommandError};
26use crate::config::{AnnotatedValue, ConfigSource};
27use crate::generic_templater::GenericTemplateLanguage;
28use crate::template_builder::TemplateLanguage as _;
29use crate::templater::TemplatePropertyExt as _;
30use crate::ui::Ui;
31
32#[derive(clap::Args, Clone, Debug)]
33#[command(group = clap::ArgGroup::new("config_level").multiple(false).required(true))]
34pub(crate) struct ConfigArgs {
35 /// Target the user-level config
36 #[arg(long, group = "config_level")]
37 user: bool,
38
39 /// Target the repo-level config
40 #[arg(long, group = "config_level")]
41 repo: bool,
42}
43
44impl ConfigArgs {
45 fn get_source_kind(&self) -> ConfigSource {
46 if self.user {
47 ConfigSource::User
48 } else if self.repo {
49 ConfigSource::Repo
50 } else {
51 // Shouldn't be reachable unless clap ArgGroup is broken.
52 panic!("No config_level provided");
53 }
54 }
55}
56
57/// Manage config options
58///
59/// Operates on jj configuration, which comes from the config file and
60/// environment variables.
61///
62/// For file locations, supported config options, and other details about jj
63/// config, see https://github.com/martinvonz/jj/blob/main/docs/config.md.
64#[derive(clap::Subcommand, Clone, Debug)]
65pub(crate) enum ConfigCommand {
66 #[command(visible_alias("l"))]
67 List(ConfigListArgs),
68 #[command(visible_alias("g"))]
69 Get(ConfigGetArgs),
70 #[command(visible_alias("s"))]
71 Set(ConfigSetArgs),
72 #[command(visible_alias("e"))]
73 Edit(ConfigEditArgs),
74 #[command(visible_alias("p"))]
75 Path(ConfigPathArgs),
76}
77
78/// List variables set in config file, along with their values.
79#[derive(clap::Args, Clone, Debug)]
80#[command(group(clap::ArgGroup::new("specific").args(&["repo", "user"])))]
81pub(crate) struct ConfigListArgs {
82 /// An optional name of a specific config option to look up.
83 #[arg(value_parser = NonEmptyStringValueParser::new())]
84 pub name: Option<String>,
85 /// Whether to explicitly include built-in default values in the list.
86 #[arg(long, conflicts_with = "specific")]
87 pub include_defaults: bool,
88 /// Allow printing overridden values.
89 #[arg(long)]
90 pub include_overridden: bool,
91 /// Target the user-level config
92 #[arg(long)]
93 user: bool,
94 /// Target the repo-level config
95 #[arg(long)]
96 repo: bool,
97 // TODO(#1047): Support --show-origin using LayeredConfigs.
98 /// Render each variable using the given template
99 ///
100 /// The following keywords are defined:
101 ///
102 /// * `name: String`: Config name.
103 /// * `value: String`: Serialized value in TOML syntax.
104 /// * `overridden: Boolean`: True if the value is shadowed by other.
105 ///
106 /// For the syntax, see https://github.com/martinvonz/jj/blob/main/docs/templates.md
107 #[arg(long, short = 'T', verbatim_doc_comment)]
108 template: Option<String>,
109}
110
111impl ConfigListArgs {
112 fn get_source_kind(&self) -> Option<ConfigSource> {
113 if self.user {
114 Some(ConfigSource::User)
115 } else if self.repo {
116 Some(ConfigSource::Repo)
117 } else {
118 //List all variables
119 None
120 }
121 }
122}
123
124/// Get the value of a given config option.
125///
126/// Unlike `jj config list`, the result of `jj config get` is printed without
127/// extra formatting and therefore is usable in scripting. For example:
128///
129/// $ jj config list user.name
130/// user.name="Martin von Zweigbergk"
131/// $ jj config get user.name
132/// Martin von Zweigbergk
133#[derive(clap::Args, Clone, Debug)]
134#[command(verbatim_doc_comment)]
135pub(crate) struct ConfigGetArgs {
136 #[arg(required = true)]
137 name: String,
138}
139
140/// Update config file to set the given option to a given value.
141#[derive(clap::Args, Clone, Debug)]
142pub(crate) struct ConfigSetArgs {
143 #[arg(required = true)]
144 name: String,
145 #[arg(required = true)]
146 value: String,
147 #[clap(flatten)]
148 config_args: ConfigArgs,
149}
150
151/// Start an editor on a jj config file.
152///
153/// Creates the file if it doesn't already exist regardless of what the editor
154/// does.
155#[derive(clap::Args, Clone, Debug)]
156pub(crate) struct ConfigEditArgs {
157 #[clap(flatten)]
158 pub config_args: ConfigArgs,
159}
160
161/// Print the path to the config file
162///
163/// A config file at that path may or may not exist.
164///
165/// See `jj config edit` if you'd like to immediately edit the file.
166#[derive(clap::Args, Clone, Debug)]
167pub(crate) struct ConfigPathArgs {
168 #[clap(flatten)]
169 pub config_args: ConfigArgs,
170}
171
172#[instrument(skip_all)]
173pub(crate) fn cmd_config(
174 ui: &mut Ui,
175 command: &CommandHelper,
176 subcommand: &ConfigCommand,
177) -> Result<(), CommandError> {
178 match subcommand {
179 ConfigCommand::List(sub_args) => cmd_config_list(ui, command, sub_args),
180 ConfigCommand::Get(sub_args) => cmd_config_get(ui, command, sub_args),
181 ConfigCommand::Set(sub_args) => cmd_config_set(ui, command, sub_args),
182 ConfigCommand::Edit(sub_args) => cmd_config_edit(ui, command, sub_args),
183 ConfigCommand::Path(sub_args) => cmd_config_path(ui, command, sub_args),
184 }
185}
186
187// AnnotatedValue will be cloned internally in the templater. If the cloning
188// cost matters, wrap it with Rc.
189fn config_template_language() -> GenericTemplateLanguage<'static, AnnotatedValue> {
190 type L = GenericTemplateLanguage<'static, AnnotatedValue>;
191 let mut language = L::new();
192 // "name" instead of "path" to avoid confusion with the source file path
193 language.add_keyword("name", |self_property| {
194 let out_property = self_property.map(|annotated| annotated.path.join("."));
195 Ok(L::wrap_string(out_property))
196 });
197 language.add_keyword("value", |self_property| {
198 // TODO: would be nice if we can provide raw dynamically-typed value
199 let out_property = self_property.map(|annotated| serialize_config_value(&annotated.value));
200 Ok(L::wrap_string(out_property))
201 });
202 language.add_keyword("overridden", |self_property| {
203 let out_property = self_property.map(|annotated| annotated.is_overridden);
204 Ok(L::wrap_boolean(out_property))
205 });
206 language
207}
208
209#[instrument(skip_all)]
210pub(crate) fn cmd_config_list(
211 ui: &mut Ui,
212 command: &CommandHelper,
213 args: &ConfigListArgs,
214) -> Result<(), CommandError> {
215 let template = {
216 let language = config_template_language();
217 let text = match &args.template {
218 Some(value) => value.to_owned(),
219 None => command
220 .settings()
221 .config()
222 .get_string("templates.config_list")?,
223 };
224 command
225 .parse_template(ui, &language, &text, GenericTemplateLanguage::wrap_self)?
226 .labeled("config_list")
227 };
228
229 ui.request_pager();
230 let mut formatter = ui.stdout_formatter();
231 let name_path = args
232 .name
233 .as_ref()
234 .map_or(vec![], |name| name.split('.').collect_vec());
235 let mut wrote_values = false;
236 for annotated in command.resolved_config_values(&name_path)? {
237 // Remove overridden values.
238 if annotated.is_overridden && !args.include_overridden {
239 continue;
240 }
241
242 if let Some(target_source) = args.get_source_kind() {
243 if target_source != annotated.source {
244 continue;
245 }
246 }
247
248 // Skip built-ins if not included.
249 if !args.include_defaults && annotated.source == ConfigSource::Default {
250 continue;
251 }
252
253 template.format(&annotated, formatter.as_mut())?;
254 wrote_values = true;
255 }
256 drop(formatter);
257 if !wrote_values {
258 // Note to stderr explaining why output is empty.
259 if let Some(name) = &args.name {
260 writeln!(ui.warning_default(), "No matching config key for {name}")?;
261 } else {
262 writeln!(ui.warning_default(), "No config to list")?;
263 }
264 }
265 Ok(())
266}
267
268#[instrument(skip_all)]
269pub(crate) fn cmd_config_get(
270 ui: &mut Ui,
271 command: &CommandHelper,
272 args: &ConfigGetArgs,
273) -> Result<(), CommandError> {
274 let value = command
275 .settings()
276 .config()
277 .get_string(&args.name)
278 .map_err(|err| match err {
279 config::ConfigError::Type {
280 origin,
281 unexpected,
282 expected,
283 key,
284 } => {
285 let expected = format!("a value convertible to {expected}");
286 // Copied from `impl fmt::Display for ConfigError`. We can't use
287 // the `Display` impl directly because `expected` is required to
288 // be a `'static str`.
289 let mut buf = String::new();
290 use std::fmt::Write;
291 write!(buf, "invalid type: {unexpected}, expected {expected}").unwrap();
292 if let Some(key) = key {
293 write!(buf, " for key `{key}`").unwrap();
294 }
295 if let Some(origin) = origin {
296 write!(buf, " in {origin}").unwrap();
297 }
298 config_error(buf)
299 }
300 err => err.into(),
301 })?;
302 writeln!(ui.stdout(), "{value}")?;
303 Ok(())
304}
305
306#[instrument(skip_all)]
307pub(crate) fn cmd_config_set(
308 _ui: &mut Ui,
309 command: &CommandHelper,
310 args: &ConfigSetArgs,
311) -> Result<(), CommandError> {
312 let config_path = get_new_config_file_path(&args.config_args.get_source_kind(), command)?;
313 if config_path.is_dir() {
314 return Err(user_error(format!(
315 "Can't set config in path {path} (dirs not supported)",
316 path = config_path.display()
317 )));
318 }
319 write_config_value_to_file(&args.name, &args.value, &config_path)
320}
321
322#[instrument(skip_all)]
323pub(crate) fn cmd_config_edit(
324 _ui: &mut Ui,
325 command: &CommandHelper,
326 args: &ConfigEditArgs,
327) -> Result<(), CommandError> {
328 let config_path = get_new_config_file_path(&args.config_args.get_source_kind(), command)?;
329 run_ui_editor(command.settings(), &config_path)
330}
331
332#[instrument(skip_all)]
333pub(crate) fn cmd_config_path(
334 ui: &mut Ui,
335 command: &CommandHelper,
336 args: &ConfigPathArgs,
337) -> Result<(), CommandError> {
338 let config_path = get_new_config_file_path(&args.config_args.get_source_kind(), command)?;
339 writeln!(
340 ui.stdout(),
341 "{}",
342 config_path
343 .to_str()
344 .ok_or_else(|| user_error("The config path is not valid UTF-8"))?
345 )?;
346 Ok(())
347}