use std::collections::BTreeMap;
use std::path::Path;
use anyhow::{Context, Result};
use serde::Deserialize;
use crate::run;
pub const FILE_NAME: &str = ".jj-fork.toml";
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
pub upstream: Upstream,
#[serde(default)]
pub fork: Fork,
#[serde(default)]
pub checks: Checks,
#[serde(default)]
pub generated: Option<Generated>,
#[serde(default)]
pub tiers: Tiers,
#[serde(default)]
pub low_memory: LowMemory,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Upstream {
#[serde(default = "default_upstream_remote")]
pub remote: String,
pub url: String,
#[serde(default = "default_main")]
pub branch: String,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Fork {
#[serde(default = "default_origin")]
pub remote: String,
#[serde(default = "default_fork_branch")]
pub branch: String,
#[serde(default = "default_series_prefixes")]
pub series_prefixes: Vec<String>,
#[serde(default = "default_glue_prefix")]
pub glue_prefix: String,
#[serde(default)]
pub mirror_branch: Option<String>,
#[serde(default = "default_merge_message")]
pub merge_message: String,
}
impl Default for Fork {
fn default() -> Self {
Fork {
remote: default_origin(),
branch: default_fork_branch(),
series_prefixes: default_series_prefixes(),
glue_prefix: default_glue_prefix(),
mirror_branch: None,
merge_message: default_merge_message(),
}
}
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Checks {
#[serde(default)]
pub patch: Vec<Check>,
#[serde(default)]
pub fork: Vec<Check>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Check {
pub name: String,
pub run: String,
#[serde(default)]
pub when: Option<String>,
#[serde(default)]
pub kind: CheckKind,
#[serde(default = "default_tier")]
pub tier: String,
#[serde(default)]
pub low_if_errors_at_most: Option<usize>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum CheckKind {
#[default]
Command,
GoTest,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Generated {
pub paths: Vec<String>,
pub regenerate: String,
#[serde(default)]
pub inputs: Vec<String>,
#[serde(default)]
pub header: Option<Header>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Header {
pub text: String,
pub comments: BTreeMap<String, String>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Tiers {
#[serde(default = "default_low_max")]
pub low_max: Limits,
#[serde(default = "default_medium_max")]
pub medium_max: Limits,
}
impl Default for Tiers {
fn default() -> Self {
Tiers {
low_max: default_low_max(),
medium_max: default_medium_max(),
}
}
}
#[derive(Debug, Clone, Copy, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Limits {
pub files: usize,
pub hunks: usize,
pub lines: usize,
pub commits: usize,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LowMemory {
#[serde(default = "default_low_memory_mib")]
pub below_mib: u64,
#[serde(default)]
pub env: BTreeMap<String, String>,
}
impl Default for LowMemory {
fn default() -> Self {
LowMemory {
below_mib: default_low_memory_mib(),
env: BTreeMap::new(),
}
}
}
fn default_upstream_remote() -> String {
"upstream".into()
}
fn default_origin() -> String {
"origin".into()
}
fn default_main() -> String {
"main".into()
}
fn default_fork_branch() -> String {
"fork/main".into()
}
fn default_series_prefixes() -> Vec<String> {
vec!["patch/".into()]
}
fn default_glue_prefix() -> String {
"glue/".into()
}
fn default_merge_message() -> String {
"internal: assemble fork patch set".into()
}
fn default_tier() -> String {
"medium".into()
}
fn default_low_max() -> Limits {
Limits {
files: 2,
hunks: 3,
lines: 60,
commits: 3,
}
}
fn default_medium_max() -> Limits {
Limits {
files: 12,
hunks: 20,
lines: 400,
commits: 10,
}
}
fn default_low_memory_mib() -> u64 {
8192
}
impl Config {
pub fn load(root: &Path, path: Option<&Path>) -> Result<Config> {
let path = path
.map(Path::to_path_buf)
.unwrap_or_else(|| root.join(FILE_NAME));
let text = std::fs::read_to_string(&path).with_context(|| {
format!(
"failed to read {}; run `jj fork init` first",
path.display()
)
})?;
let mut table: toml::Table =
toml::from_str(&text).with_context(|| format!("invalid {}", path.display()))?;
let overrides = run::output(root, "jj", &["config", "list", "jj-fork"]).unwrap_or_default();
merge(&mut table, parse_overrides(&overrides)?);
let config: Config = toml::Value::Table(table)
.try_into()
.with_context(|| format!("invalid configuration in {} or jj config", path.display()))?;
Ok(config)
}
pub fn upstream_ref(&self) -> String {
format!("{}@{}", self.upstream.branch, self.upstream.remote)
}
}
fn parse_overrides(listing: &str) -> Result<toml::Table> {
let dotted: String = listing
.lines()
.filter_map(|line| line.strip_prefix("jj-fork."))
.map(|line| format!("{line}\n"))
.collect();
toml::from_str(&dotted).context("invalid jj-fork overrides in jj config")
}
fn merge(base: &mut toml::Table, overrides: toml::Table) {
for (key, value) in overrides {
match (base.get_mut(&key), value) {
(Some(toml::Value::Table(base)), toml::Value::Table(value)) => merge(base, value),
(_, value) => {
base.insert(key, value);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn overrides_merge_into_nested_tables() {
let mut base: toml::Table = toml::from_str(
"[upstream]\nurl = \"a\"\nbranch = \"main\"\n[fork]\nremote = \"origin\"\n",
)
.unwrap();
let overrides = parse_overrides(
"jj-fork.fork.remote = \"mine\"\njj-fork.upstream.branch = \"trunk\"\nother.key = 1\n",
)
.unwrap();
merge(&mut base, overrides);
let config: Config = toml::Value::Table(base).try_into().unwrap();
assert_eq!(config.fork.remote, "mine");
assert_eq!(config.upstream.branch, "trunk");
assert_eq!(config.upstream.url, "a");
assert_eq!(config.fork.branch, "fork/main");
}
#[test]
fn unknown_keys_are_rejected() {
let result: Result<Config, _> = toml::from_str("[upstream]\nurl = \"a\"\nbrnach = \"x\"\n");
assert!(result.is_err());
}
}