use crate::branch::PyBranch;
use crate::revisionid::RevisionId;
use pyo3::prelude::*;
use std::sync::OnceLock;
#[derive(Debug)]
pub enum SearchError {
Unavailable,
NoIndex,
Other(crate::error::Error),
}
impl std::fmt::Display for SearchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SearchError::Unavailable => write!(f, "breezy search plugin is not installed"),
SearchError::NoIndex => write!(f, "branch has no search index"),
SearchError::Other(e) => write!(f, "breezy error: {e}"),
}
}
}
impl std::error::Error for SearchError {}
impl From<crate::error::Error> for SearchError {
fn from(e: crate::error::Error) -> Self {
SearchError::Other(e)
}
}
pub fn is_available() -> bool {
static CELL: OnceLock<bool> = OnceLock::new();
*CELL.get_or_init(|| Python::attach(|py| py.import("breezy.plugins.search.index").is_ok()))
}
#[derive(Debug, Clone)]
pub enum Hit {
Revision(RevisionId),
FileText {
revision: RevisionId,
path: String,
},
}
pub fn suggest<B: PyBranch>(branch: &B, query: &str) -> Result<Vec<String>, SearchError> {
if !is_available() {
return Err(SearchError::Unavailable);
}
Python::attach(|py| -> PyResult<Vec<String>> {
let m = py.import("breezy.plugins.search.index")?;
let errors = py.import("breezy.plugins.search.errors")?;
let open_index_branch = m.getattr("open_index_branch")?;
let py_branch = branch.to_object(py);
let index = match open_index_branch.call1((py_branch,)) {
Ok(i) => i,
Err(e) => {
let no_index = errors.getattr("NoSearchIndex")?;
if e.is_instance(py, &no_index) {
return Ok(Vec::new());
}
return Err(e);
}
};
let terms: Vec<(String,)> = {
let query_tuples: Vec<(String,)> =
query.split_whitespace().map(|t| (t.to_string(),)).collect();
let raw = index.call_method1("suggest", (query_tuples,))?;
raw.extract()?
};
let mut out: Vec<String> = terms.into_iter().map(|(t,)| t).collect();
out.sort();
out.dedup();
Ok(out)
})
.map_err(|e| SearchError::Other(e.into()))
.and_then(|v| {
if v.is_empty() {
Err(SearchError::NoIndex)
} else {
Ok(v)
}
})
}
pub fn search<B: PyBranch>(branch: &B, query: &str) -> Result<Vec<Hit>, SearchError> {
if !is_available() {
return Err(SearchError::Unavailable);
}
Python::attach(|py| -> PyResult<Vec<Hit>> {
let m = py.import("breezy.plugins.search.index")?;
let errors = py.import("breezy.plugins.search.errors")?;
let open_index_branch = m.getattr("open_index_branch")?;
let py_branch = branch.to_object(py);
let index = match open_index_branch.call1((py_branch,)) {
Ok(i) => i,
Err(e) => {
let no_index = errors.getattr("NoSearchIndex")?;
if e.is_instance(py, &no_index) {
return Ok(Vec::new());
}
return Err(e);
}
};
let file_hit = m.getattr("FileTextHit")?;
let revision_hit = m.getattr("RevisionHit")?;
let query_tuples: Vec<(String,)> =
query.split_whitespace().map(|t| (t.to_string(),)).collect();
let results = index.call_method1("search", (query_tuples,))?;
let mut hits = Vec::new();
for item in results.try_iter()? {
let item = item?;
if item.is_instance(&file_hit)? {
let tk = item.getattr("text_key")?;
let revision: RevisionId = tk.get_item(1)?.extract()?;
let file_id_bytes: Vec<u8> = tk.get_item(0)?.extract()?;
hits.push(Hit::FileText {
revision,
path: String::from_utf8_lossy(&file_id_bytes).into_owned(),
});
} else if item.is_instance(&revision_hit)? {
let rk = item.getattr("revision_key")?;
let revision: RevisionId = rk.get_item(0)?.extract()?;
hits.push(Hit::Revision(revision));
}
}
Ok(hits)
})
.map_err(|e| SearchError::Other(e.into()))
}