use rmcp::ErrorData as McpError;
use rmcp::model::CallToolResult;
use super::ServerState;
use super::helpers::{
LOG_LIMIT_DEFAULT, LOG_LIMIT_MAX, LOG_WALK_MAX, git_history_if_fresh, head_sha, head_snapshot_id, json_result,
require_git_repo,
};
use super::types::{GitCommitHit, SearchGitHistoryParams, SearchGitHistoryResponse};
use crate::git::CommitInfo;
use crate::git_history::fts::{self, FtsScope};
use crate::path::RelPath;
pub(super) fn reject_external_path(path: &RelPath) -> Result<(), McpError> {
if path.is_external() {
return Err(McpError::invalid_params(
format!(
"path is outside the git repository (indexed via scan.extra_roots); \
blame is unavailable for external files: {path}"
),
None,
));
}
Ok(())
}
pub(super) fn run_search_git_history(
state: &ServerState,
params: SearchGitHistoryParams,
) -> Result<CallToolResult, McpError> {
let repo = require_git_repo(state)?;
let limit = params.limit.unwrap_or(LOG_LIMIT_DEFAULT).min(LOG_LIMIT_MAX) as usize;
let scope = FtsScope::parse(params.field.as_deref());
let head = head_sha(repo)?;
let snapshot = head_snapshot_id(&head);
let skip = match params.cursor.as_ref() {
Some(cursor) => {
let (offset, snapshot_id) = cursor.decode_in_memory()?;
if snapshot_id != snapshot {
return json_result(&SearchGitHistoryResponse {
commits: Vec::new(),
partial: false,
next_cursor: None,
cursor_invalidated: true,
});
}
offset as usize
}
None => 0,
};
let want = limit.saturating_add(1);
let (mut hits, partial) = match git_history_if_fresh(state, &head) {
Some(index) => (index.search_commits(¶ms.pattern, scope, skip, want), false),
None => {
let mut query_terms = ahash::AHashSet::new();
fts::tokenize(¶ms.pattern, &mut query_terms);
let window = LOG_WALK_MAX as u32;
let live = state
.git_cache
.log(repo, &head, None, window, false)
.map_err(|e| McpError::internal_error(format!("log: {e}"), None))?;
let matched: Vec<CommitInfo> = live
.iter()
.filter(|c| fts::commit_matches_terms(c, &query_terms, scope))
.skip(skip)
.take(want)
.cloned()
.collect();
(matched, true)
}
};
let has_more = hits.len() > limit;
hits.truncate(limit);
let next_cursor = has_more.then(|| super::cursor::Cursor::encode_in_memory((skip + hits.len()) as u64, snapshot));
let commits: Vec<GitCommitHit> = hits.into_iter().map(commit_to_hit).collect();
json_result(&SearchGitHistoryResponse {
commits,
partial,
next_cursor,
cursor_invalidated: false,
})
}
fn commit_to_hit(c: CommitInfo) -> GitCommitHit {
GitCommitHit {
sha: c.sha,
short_sha: c.short_sha,
summary: c.summary,
author: c.author,
author_email: c.author_email,
author_time_unix: c.author_time_unix,
body: c.body,
}
}