Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/config.default.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,14 @@ enabled = true
# Set to null to disable threshold filtering (return all results).
# Typical values: 0.3-0.5 for l2/cosine, 0.7-0.9 for ip
# threshold = 0.4

[databases]
# Additional database paths to include in read-only cross-repo queries.
# The primary database (current repo) is always included.
# This setting is only recognized in .codemark/config.toml (local repo config).
# Supports ~ expansion and relative paths (relative to repo root).
# Additional databases are read-only; write operations affect only the primary DB.
# additional = [
# "../shared-library/.codemark/codemark.db",
# "~/projects/dependency-repo/.codemark/codemark.db",
# ]
46 changes: 38 additions & 8 deletions src/cli/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,19 +173,49 @@ fn load_config(cli: &Cli) -> Config {
///
/// Returns (source_label, database) pairs. Falls back to single auto-detected db.
/// Returns Error::NotInitialized if any of the specified databases do not exist.
///
/// Database loading strategy:
/// 1. If CLI `--db` is specified, only those paths are used (override mode)
/// 2. Otherwise, uses auto-detected primary DB + configured additional databases
fn open_all_dbs(cli: &Cli) -> Result<Vec<(String, Database)>> {
if cli.db.is_empty() {
let db = open_db(cli)?;
let label = source_label_from_cli(cli);
return Ok(vec![(label, db)]);
// If CLI specified explicit --db paths, use only those (override mode)
if !cli.db.is_empty() {
let mut dbs = Vec::new();
for path in &cli.db {
if path.exists() {
let label = source_label_from_path(path);
dbs.push((label, Database::open(path)?));
}
}
return Ok(dbs);
}

// No CLI --db specified: use auto-detected primary + configured additional
let mut dbs = Vec::new();
for path in &cli.db {
if path.exists() {
let label = source_label_from_path(path);
dbs.push((label, Database::open(path)?));

// Always include primary DB (auto-detected from git root)
let primary_db = open_db(cli)?;
let primary_label = source_label_from_cli(cli);
dbs.push((primary_label, primary_db));

// Load additional DBs from local config
let cwd = std::env::current_dir()?;
if let Some(ctx) = git_context::detect_context(&cwd) {
let codemark_dir = ctx.repo_root.join(".codemark");
let config = Config::load_layered(&codemark_dir);
let additional_paths = config.databases.resolve_additional_paths(&ctx.repo_root);

for path in additional_paths {
if path.exists() {
let label = source_label_from_path(&path);
// Only add if not already present (avoid duplicates)
if !dbs.iter().any(|(l, _)| l == &label) {
dbs.push((label, Database::open(&path)?));
}
}
}
}

Ok(dbs)
}

Expand Down
142 changes: 142 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ pub struct Config {
pub semantic: SemanticConfig,
#[serde(default)]
pub open: OpenConfig,
#[serde(default)]
pub databases: DatabasesConfig,
}

/// Semantic search configuration wrapper.
Expand Down Expand Up @@ -146,6 +148,82 @@ impl HealthConfig {
}
}

/// Additional database paths configuration for cross-repo queries.
///
/// This configuration is only supported in local config (`.codemark/config.toml`),
/// not in global config. Each repo defines its own related databases.
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(default)]
pub struct DatabasesConfig {
/// Additional database paths for cross-repo queries.
/// Paths are relative to the repo root containing `.codemark/config.toml`.
/// Supports `~` expansion for home directory.
pub additional: Vec<String>,
}

impl DatabasesConfig {
/// Resolve additional database paths, expanding `~` and resolving relative paths.
///
/// `repo_root` is the git repository root containing the `.codemark` directory.
/// Relative paths are resolved from the repo root (not from `.codemark/`).
pub fn resolve_additional_paths(&self, repo_root: &Path) -> Vec<PathBuf> {
self.additional
.iter()
.map(|p| {
let expanded = shellexpand::tilde(p);
let path = PathBuf::from(expanded.as_ref());
let resolved = if path.is_absolute() {
path
} else {
// Relative to repo root (where .codemark/ lives)
repo_root.join(path)
};
// Normalize the path to resolve . and .. components
normalize_path(&resolved)
})
.collect()
}
}

/// Normalize a path by resolving . and .. components.
/// Unlike std::fs::canonicalize, this doesn't require the path to exist.
fn normalize_path(path: &Path) -> PathBuf {
use std::path::Component;

let mut result = PathBuf::new();
let mut has_root = false;

for component in path.components() {
match component {
Component::Prefix(prefix) => {
// Prefix needs special handling - it's part of the path's structure
// We need to reconstruct it properly
let mut temp = PathBuf::new();
temp.push(prefix.as_os_str());
result = temp;
}
Component::RootDir => {
if !has_root {
result.push(component);
has_root = true;
}
}
Component::CurDir => {
// Skip . components
}
Component::ParentDir => {
// Pop if possible, but don't go above root
if !result.pop() || !has_root {
// If we can't pop, we're at root - keep the ..
result.push(component);
}
}
Component::Normal(normal) => result.push(normal),
}
}
if result.as_os_str().is_empty() { PathBuf::from(".") } else { result }
}

/// Editor configuration for the `codemark open` command.
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(default)]
Expand Down Expand Up @@ -367,6 +445,12 @@ impl Config {
self.open.editor_types.gui.push(editor);
}
}

// Databases config - local-only setting, replace entire section if set
// This is only respected in local config (.codemark/config.toml)
if !other.databases.additional.is_empty() {
self.databases.additional = other.databases.additional;
}
}

/// Write the default config file to the global config directory.
Expand Down Expand Up @@ -825,4 +909,62 @@ terminal = ["vim", "emacs"]
global.open.editor_types.terminal.iter().filter(|x| x.as_str() == "vim").count();
assert_eq!(vim_count, 1);
}

#[test]
fn parse_databases_config() {
let toml = r#"
[databases]
additional = [
"../shared-lib/.codemark/codemark.db",
"~/projects/another-repo/.codemark/codemark.db",
]
"#;
let config: Config = toml::from_str(toml).unwrap();
assert_eq!(config.databases.additional.len(), 2);
assert_eq!(config.databases.additional[0], "../shared-lib/.codemark/codemark.db");
assert_eq!(config.databases.additional[1], "~/projects/another-repo/.codemark/codemark.db");
}

#[test]
fn resolve_additional_paths_empty() {
let config = DatabasesConfig::default();
let repo_root = Path::new("/test/repo");
let paths = config.resolve_additional_paths(repo_root);
assert!(paths.is_empty());
}

#[test]
fn resolve_additional_paths_relative() {
let mut config = DatabasesConfig::default();
config.additional =
vec!["../shared-lib/.codemark/codemark.db".to_string(), "./local.db".to_string()];
let repo_root = Path::new("/test/repo");
let paths = config.resolve_additional_paths(repo_root);
assert_eq!(paths.len(), 2);
// Paths should be normalized
assert_eq!(paths[0], PathBuf::from("/test/shared-lib/.codemark/codemark.db"));
assert_eq!(paths[1], PathBuf::from("/test/repo/local.db"));
}

#[test]
fn resolve_additional_paths_absolute() {
let mut config = DatabasesConfig::default();
config.additional = vec!["/absolute/path/to/db.sqlite".to_string()];
let repo_root = Path::new("/test/repo");
let paths = config.resolve_additional_paths(repo_root);
assert_eq!(paths.len(), 1);
assert_eq!(paths[0], PathBuf::from("/absolute/path/to/db.sqlite"));
}

#[test]
fn resolve_additional_paths_tilde_expansion() {
let mut config = DatabasesConfig::default();
config.additional = vec!["~/projects/db.sqlite".to_string()];
let repo_root = Path::new("/test/repo");
let paths = config.resolve_additional_paths(repo_root);
assert_eq!(paths.len(), 1);
// Tilde should be expanded to the home directory
assert!(paths[0].to_string_lossy().contains("projects"));
assert!(paths[0].is_absolute());
}
}
Loading
Loading