1//! Explicit first-run configuration, independent of repository discovery.
7use j::parse::parse_config;
14const IDENTITY_EXAMPLE: &str = "user = { name = \"Ada Lovelace\", email = \"ada@example.org\" }";
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.
19pub 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()));
23 if path.symlink_metadata().is_ok() {
24 return existing(path, args);
26 let source = match args {
27 [name, email] => with_identity(default, name, email)?,
28 [] => default.to_string(),
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) {
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))),
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,
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)?,
56fn existing(path: &Path, args: &[&str]) -> Result<String, (u8, String)> {
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`.",
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,
69 Err((_, message)) => message,
71 Err(error) => format!("cannot read this config: {}", error),
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,
79fn 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)
84fn 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),
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))
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,
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)?;
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list