use std::path::PathBuf;
use serde::Deserialize;
use crate::{
error::{Error, Result},
jj::run_jj_command,
};
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum StackFormat {
Linear,
}
fn default_remote_name() -> String {
"origin".to_string()
}
fn default_branch() -> String {
"main".to_string()
}
fn default_true() -> bool {
true
}
fn default_stack_format() -> StackFormat {
StackFormat::Linear
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Config {
pub gitlab_host: String,
pub gitlab_project: String,
pub gitlab_token: String,
#[serde(default = "default_remote_name")]
pub remote_name: String,
#[serde(default = "default_branch")]
pub default_branch: String,
#[serde(default)]
pub ca_bundle: Option<String>,
#[serde(default)]
pub tls_accept_non_compliant_certs: bool,
#[serde(default = "default_true")]
pub enable_stack_visualization: bool,
#[serde(default = "default_stack_format")]
pub stack_format: StackFormat,
#[serde(default = "default_true")]
pub delete_source_branch: bool,
#[serde(default)]
pub squash_commits: bool,
#[serde(default)]
pub assign_to_self: bool,
#[serde(default)]
pub default_reviewers: Vec<String>,
}
impl Config {
pub fn load(repo_path: &PathBuf) -> Result<Self> {
let output = run_jj_command(repo_path, &["config", "list"])?;
let toml_value: toml::Value =
toml::from_str(&output.stdout).map_err(|e| Error::Config {
message: format!("Failed to parse config as TOML: {}", e),
})?;
let jj_vine_value = toml_value.get("jj-vine").ok_or_else(|| Error::Config {
message: "Missing required config section: jj-vine".to_string(),
})?;
let config: Config = jj_vine_value
.clone()
.try_into()
.map_err(|e| Error::Config {
message: format!("Failed to parse jj-vine config: {}", e),
})?;
Ok(config)
}
}
#[cfg(test)]
mod tests {
use tempfile::TempDir;
use super::*;
fn create_test_repo() -> (TempDir, PathBuf) {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let repo_path = temp_dir.path().to_path_buf();
run_jj_command(&repo_path, &["git", "init", "--colocate"]).expect("Failed to init jj repo");
(temp_dir, repo_path)
}
#[test]
fn test_config_load_missing_required() {
let (_temp, repo_path) = create_test_repo();
let result = Config::load(&repo_path);
assert!(result.is_err());
if let Err(Error::Config { message }) = result {
assert!(
message.contains("missing field")
|| message.contains("gitlab")
|| message.contains("jj-vine"),
"Error should mention missing field, got: {}",
message
);
} else {
panic!("Expected Config error for missing required field");
}
}
#[test]
fn test_config_load_complete() {
let (_temp, repo_path) = create_test_repo();
run_jj_command(
&repo_path,
&[
"config",
"set",
"--repo",
"jj-vine.gitlabHost",
"https://gitlab.example.com",
],
)
.expect("Failed to set config");
run_jj_command(
&repo_path,
&[
"config",
"set",
"--repo",
"jj-vine.gitlabProject",
"my-group/my-project",
],
)
.expect("Failed to set config");
run_jj_command(
&repo_path,
&[
"config",
"set",
"--repo",
"jj-vine.gitlabToken",
"glpat-test123",
],
)
.expect("Failed to set config");
let config = Config::load(&repo_path).expect("Failed to load config");
assert_eq!(config.gitlab_host, "https://gitlab.example.com");
assert_eq!(config.gitlab_project, "my-group/my-project");
assert_eq!(config.gitlab_token, "glpat-test123");
assert_eq!(config.remote_name, "origin");
assert_eq!(config.default_branch, "main");
}
#[test]
fn test_config_with_optional_fields() {
let (_temp, repo_path) = create_test_repo();
run_jj_command(
&repo_path,
&[
"config",
"set",
"--repo",
"jj-vine.gitlabHost",
"https://gitlab.example.com",
],
)
.expect("Failed to set config");
run_jj_command(
&repo_path,
&[
"config",
"set",
"--repo",
"jj-vine.gitlabProject",
"my-group/my-project",
],
)
.expect("Failed to set config");
run_jj_command(
&repo_path,
&[
"config",
"set",
"--repo",
"jj-vine.gitlabToken",
"glpat-test123",
],
)
.expect("Failed to set config");
run_jj_command(
&repo_path,
&["config", "set", "--repo", "jj-vine.branchPrefix", "mrs/"],
)
.expect("Failed to set config");
run_jj_command(
&repo_path,
&["config", "set", "--repo", "jj-vine.remoteName", "upstream"],
)
.expect("Failed to set config");
run_jj_command(
&repo_path,
&["config", "set", "--repo", "jj-vine.defaultBranch", "master"],
)
.expect("Failed to set config");
let config = Config::load(&repo_path).expect("Failed to load config");
assert_eq!(config.gitlab_host, "https://gitlab.example.com");
assert_eq!(config.gitlab_project, "my-group/my-project");
assert_eq!(config.gitlab_token, "glpat-test123");
assert_eq!(config.remote_name, "upstream");
assert_eq!(config.default_branch, "master");
}
#[test]
fn test_config_default_stack_visualization() {
let (_temp, repo_path) = create_test_repo();
run_jj_command(
&repo_path,
&[
"config",
"set",
"--repo",
"jj-vine.gitlabHost",
"https://gitlab.com",
],
)
.expect("Failed to set config");
run_jj_command(
&repo_path,
&[
"config",
"set",
"--repo",
"jj-vine.gitlabProject",
"test/proj",
],
)
.expect("Failed to set config");
run_jj_command(
&repo_path,
&["config", "set", "--repo", "jj-vine.gitlabToken", "token"],
)
.expect("Failed to set config");
let config = Config::load(&repo_path).expect("Failed to load config");
assert!(config.enable_stack_visualization);
assert!(matches!(config.stack_format, StackFormat::Linear));
}
#[test]
fn test_config_explicit_stack_visualization() {
let (_temp, repo_path) = create_test_repo();
run_jj_command(
&repo_path,
&[
"config",
"set",
"--repo",
"jj-vine.gitlabHost",
"https://gitlab.com",
],
)
.expect("Failed to set config");
run_jj_command(
&repo_path,
&[
"config",
"set",
"--repo",
"jj-vine.gitlabProject",
"test/proj",
],
)
.expect("Failed to set config");
run_jj_command(
&repo_path,
&["config", "set", "--repo", "jj-vine.gitlabToken", "token"],
)
.expect("Failed to set config");
run_jj_command(
&repo_path,
&[
"config",
"set",
"--repo",
"jj-vine.enableStackVisualization",
"false",
],
)
.expect("Failed to set config");
let config = Config::load(&repo_path).expect("Failed to load config");
assert!(!config.enable_stack_visualization);
assert!(matches!(config.stack_format, StackFormat::Linear)); }
#[test]
fn test_config_default_mr_settings() {
let (_temp, repo_path) = create_test_repo();
run_jj_command(
&repo_path,
&[
"config",
"set",
"--repo",
"jj-vine.gitlabHost",
"https://gitlab.com",
],
)
.expect("Failed to set config");
run_jj_command(
&repo_path,
&[
"config",
"set",
"--repo",
"jj-vine.gitlabProject",
"test/proj",
],
)
.expect("Failed to set config");
run_jj_command(
&repo_path,
&["config", "set", "--repo", "jj-vine.gitlabToken", "token"],
)
.expect("Failed to set config");
let config = Config::load(&repo_path).expect("Failed to load config");
assert!(config.delete_source_branch);
assert!(!config.squash_commits);
}
#[test]
fn test_config_explicit_mr_settings() {
let (_temp, repo_path) = create_test_repo();
run_jj_command(
&repo_path,
&[
"config",
"set",
"--repo",
"jj-vine.gitlabHost",
"https://gitlab.com",
],
)
.expect("Failed to set config");
run_jj_command(
&repo_path,
&[
"config",
"set",
"--repo",
"jj-vine.gitlabProject",
"test/proj",
],
)
.expect("Failed to set config");
run_jj_command(
&repo_path,
&["config", "set", "--repo", "jj-vine.gitlabToken", "token"],
)
.expect("Failed to set config");
run_jj_command(
&repo_path,
&[
"config",
"set",
"--repo",
"jj-vine.deleteSourceBranch",
"false",
],
)
.expect("Failed to set config");
run_jj_command(
&repo_path,
&["config", "set", "--repo", "jj-vine.squashCommits", "true"],
)
.expect("Failed to set config");
let config = Config::load(&repo_path).expect("Failed to load config");
assert!(!config.delete_source_branch);
assert!(config.squash_commits);
}
#[test]
fn test_config_default_assign_to_self() {
let (_temp, repo_path) = create_test_repo();
run_jj_command(
&repo_path,
&[
"config",
"set",
"--repo",
"jj-vine.gitlabHost",
"https://gitlab.com",
],
)
.expect("Failed to set config");
run_jj_command(
&repo_path,
&[
"config",
"set",
"--repo",
"jj-vine.gitlabProject",
"test/proj",
],
)
.expect("Failed to set config");
run_jj_command(
&repo_path,
&["config", "set", "--repo", "jj-vine.gitlabToken", "token"],
)
.expect("Failed to set config");
let config = Config::load(&repo_path).expect("Failed to load config");
assert!(!config.assign_to_self);
}
#[test]
fn test_config_explicit_assign_to_self() {
let (_temp, repo_path) = create_test_repo();
run_jj_command(
&repo_path,
&[
"config",
"set",
"--repo",
"jj-vine.gitlabHost",
"https://gitlab.com",
],
)
.expect("Failed to set config");
run_jj_command(
&repo_path,
&[
"config",
"set",
"--repo",
"jj-vine.gitlabProject",
"test/proj",
],
)
.expect("Failed to set config");
run_jj_command(
&repo_path,
&["config", "set", "--repo", "jj-vine.gitlabToken", "token"],
)
.expect("Failed to set config");
run_jj_command(
&repo_path,
&["config", "set", "--repo", "jj-vine.assignToSelf", "true"],
)
.expect("Failed to set config");
let config = Config::load(&repo_path).expect("Failed to load config");
assert!(config.assign_to_self);
}
#[test]
fn test_config_default_reviewers_empty() {
let (_temp, repo_path) = create_test_repo();
run_jj_command(
&repo_path,
&[
"config",
"set",
"--repo",
"jj-vine.gitlabHost",
"https://gitlab.com",
],
)
.expect("Failed to set config");
run_jj_command(
&repo_path,
&[
"config",
"set",
"--repo",
"jj-vine.gitlabProject",
"test/proj",
],
)
.expect("Failed to set config");
run_jj_command(
&repo_path,
&["config", "set", "--repo", "jj-vine.gitlabToken", "token"],
)
.expect("Failed to set config");
let config = Config::load(&repo_path).expect("Failed to load config");
assert!(config.default_reviewers.is_empty());
}
#[test]
fn test_config_default_reviewers_single() {
let (_temp, repo_path) = create_test_repo();
run_jj_command(
&repo_path,
&[
"config",
"set",
"--repo",
"jj-vine.gitlabHost",
"https://gitlab.com",
],
)
.expect("Failed to set config");
run_jj_command(
&repo_path,
&[
"config",
"set",
"--repo",
"jj-vine.gitlabProject",
"test/proj",
],
)
.expect("Failed to set config");
run_jj_command(
&repo_path,
&["config", "set", "--repo", "jj-vine.gitlabToken", "token"],
)
.expect("Failed to set config");
run_jj_command(
&repo_path,
&[
"config",
"set",
"--repo",
"jj-vine.defaultReviewers",
r#"["reviewer1"]"#,
],
)
.expect("Failed to set config");
let config = Config::load(&repo_path).expect("Failed to load config");
assert_eq!(config.default_reviewers, vec!["reviewer1"]);
}
#[test]
fn test_config_default_reviewers_multiple() {
let (_temp, repo_path) = create_test_repo();
run_jj_command(
&repo_path,
&[
"config",
"set",
"--repo",
"jj-vine.gitlabHost",
"https://gitlab.com",
],
)
.expect("Failed to set config");
run_jj_command(
&repo_path,
&[
"config",
"set",
"--repo",
"jj-vine.gitlabProject",
"test/proj",
],
)
.expect("Failed to set config");
run_jj_command(
&repo_path,
&["config", "set", "--repo", "jj-vine.gitlabToken", "token"],
)
.expect("Failed to set config");
run_jj_command(
&repo_path,
&[
"config",
"set",
"--repo",
"jj-vine.defaultReviewers",
r#"["reviewer1", "reviewer2", "reviewer3"]"#,
],
)
.expect("Failed to set config");
let config = Config::load(&repo_path).expect("Failed to load config");
assert_eq!(
config.default_reviewers,
vec!["reviewer1", "reviewer2", "reviewer3"]
);
}
}