Integrates the defaultProject config field across the entire CLI surface so that omitting `-p` now falls back to the configured default. Init command: - New `--default-project` flag on `lore init` (and robot-mode variant) - InitInputs.default_project: Option<String> passed through to run_init - Validation in run_init ensures the default matches a configured path - Interactive mode: when multiple projects are configured, prompts whether to set a default and which project to use - Robot mode: InitOutputJson now includes default_project (omitted when null) for downstream automation - Autocorrect dictionary updated with `--default-project` Command handlers applying effective_project(): - handle_issues: list filters use config default when -p omitted - handle_mrs: same cascading resolution for MR listing - handle_ingest: dry-run and full sync respect the default - handle_timeline: TimelineParams.project resolved via effective_project - handle_search: SearchCliFilters.project resolved via effective_project - handle_generate_docs: project filter cascades - handle_who: falls back to config.default_project when -p omitted - handle_count: both count subcommands respect the default - handle_discussions: discussion count filters respect the default Robot-docs: - init command schema updated with --default-project flag and response_schema showing default_project as string? - New config_notes section documents the defaultProject field with type, description, and example Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
175 lines
5.2 KiB
Rust
175 lines
5.2 KiB
Rust
use std::fs;
|
|
|
|
use crate::core::config::{MinimalConfig, MinimalGitLabConfig, ProjectConfig};
|
|
use crate::core::db::{create_connection, run_migrations};
|
|
use crate::core::error::{LoreError, Result};
|
|
use crate::core::paths::{get_config_path, get_data_dir};
|
|
use crate::gitlab::{GitLabClient, GitLabProject};
|
|
|
|
pub struct InitInputs {
|
|
pub gitlab_url: String,
|
|
pub token_env_var: String,
|
|
pub project_paths: Vec<String>,
|
|
pub default_project: Option<String>,
|
|
}
|
|
|
|
pub struct InitOptions {
|
|
pub config_path: Option<String>,
|
|
pub force: bool,
|
|
pub non_interactive: bool,
|
|
}
|
|
|
|
pub struct InitResult {
|
|
pub config_path: String,
|
|
pub data_dir: String,
|
|
pub user: UserInfo,
|
|
pub projects: Vec<ProjectInfo>,
|
|
pub default_project: Option<String>,
|
|
}
|
|
|
|
pub struct UserInfo {
|
|
pub username: String,
|
|
pub name: String,
|
|
}
|
|
|
|
pub struct ProjectInfo {
|
|
pub path: String,
|
|
pub name: String,
|
|
}
|
|
|
|
pub async fn run_init(inputs: InitInputs, options: InitOptions) -> Result<InitResult> {
|
|
let config_path = get_config_path(options.config_path.as_deref());
|
|
let data_dir = get_data_dir();
|
|
|
|
if config_path.exists() && !options.force {
|
|
if options.non_interactive {
|
|
return Err(LoreError::Other(format!(
|
|
"Config file exists at {}. Use --force to overwrite.",
|
|
config_path.display()
|
|
)));
|
|
}
|
|
|
|
return Err(LoreError::Other(
|
|
"User cancelled config overwrite.".to_string(),
|
|
));
|
|
}
|
|
|
|
if url::Url::parse(&inputs.gitlab_url).is_err() {
|
|
return Err(LoreError::Other(format!(
|
|
"Invalid GitLab URL: {}",
|
|
inputs.gitlab_url
|
|
)));
|
|
}
|
|
|
|
let token = std::env::var(&inputs.token_env_var).map_err(|_| LoreError::TokenNotSet {
|
|
env_var: inputs.token_env_var.clone(),
|
|
})?;
|
|
|
|
let client = GitLabClient::new(&inputs.gitlab_url, &token, None);
|
|
|
|
let gitlab_user = client.get_current_user().await.map_err(|e| {
|
|
if matches!(e, LoreError::GitLabAuthFailed) {
|
|
LoreError::Other(format!("Authentication failed for {}", inputs.gitlab_url))
|
|
} else {
|
|
e
|
|
}
|
|
})?;
|
|
|
|
let user = UserInfo {
|
|
username: gitlab_user.username,
|
|
name: gitlab_user.name,
|
|
};
|
|
|
|
let mut validated_projects: Vec<(ProjectInfo, GitLabProject)> = Vec::new();
|
|
|
|
for project_path in &inputs.project_paths {
|
|
let project = client.get_project(project_path).await.map_err(|e| {
|
|
if matches!(e, LoreError::GitLabNotFound { .. }) {
|
|
LoreError::Other(format!("Project not found: {project_path}"))
|
|
} else {
|
|
e
|
|
}
|
|
})?;
|
|
|
|
validated_projects.push((
|
|
ProjectInfo {
|
|
path: project_path.clone(),
|
|
name: project.name.clone().unwrap_or_else(|| {
|
|
project_path
|
|
.split('/')
|
|
.next_back()
|
|
.unwrap_or(project_path)
|
|
.to_string()
|
|
}),
|
|
},
|
|
project,
|
|
));
|
|
}
|
|
|
|
// Validate default_project matches one of the configured project paths
|
|
if let Some(ref dp) = inputs.default_project {
|
|
let matched = inputs.project_paths.iter().any(|p| {
|
|
p.eq_ignore_ascii_case(dp)
|
|
|| p.to_ascii_lowercase()
|
|
.ends_with(&format!("/{}", dp.to_ascii_lowercase()))
|
|
});
|
|
if !matched {
|
|
return Err(LoreError::Other(format!(
|
|
"defaultProject '{dp}' does not match any configured project path"
|
|
)));
|
|
}
|
|
}
|
|
|
|
if let Some(parent) = config_path.parent() {
|
|
fs::create_dir_all(parent)?;
|
|
}
|
|
|
|
let config = MinimalConfig {
|
|
gitlab: MinimalGitLabConfig {
|
|
base_url: inputs.gitlab_url,
|
|
token_env_var: inputs.token_env_var,
|
|
},
|
|
projects: inputs
|
|
.project_paths
|
|
.iter()
|
|
.map(|p| ProjectConfig { path: p.clone() })
|
|
.collect(),
|
|
default_project: inputs.default_project.clone(),
|
|
};
|
|
|
|
let config_json = serde_json::to_string_pretty(&config)?;
|
|
fs::write(&config_path, format!("{config_json}\n"))?;
|
|
|
|
fs::create_dir_all(&data_dir)?;
|
|
|
|
let db_path = data_dir.join("lore.db");
|
|
let conn = create_connection(&db_path)?;
|
|
|
|
run_migrations(&conn)?;
|
|
|
|
for (_, gitlab_project) in &validated_projects {
|
|
conn.execute(
|
|
"INSERT INTO projects (gitlab_project_id, path_with_namespace, default_branch, web_url)
|
|
VALUES (?, ?, ?, ?)
|
|
ON CONFLICT(gitlab_project_id) DO UPDATE SET
|
|
path_with_namespace = excluded.path_with_namespace,
|
|
default_branch = excluded.default_branch,
|
|
web_url = excluded.web_url",
|
|
(
|
|
gitlab_project.id,
|
|
&gitlab_project.path_with_namespace,
|
|
&gitlab_project.default_branch,
|
|
&gitlab_project.web_url,
|
|
),
|
|
)?;
|
|
}
|
|
|
|
Ok(InitResult {
|
|
config_path: config_path.display().to_string(),
|
|
data_dir: data_dir.display().to_string(),
|
|
user,
|
|
projects: validated_projects.into_iter().map(|(p, _)| p).collect(),
|
|
default_project: inputs.default_project,
|
|
})
|
|
}
|