81edfe7
use async_graphql::{Context, Object, Result};
use crate::GitRepository;
use super::types::{
BlameObj, BlameRangeObj, BlobObj, CommitObj, ComparisonObj, ComparisonStatus,
ContributorStatObj, DiffObj, DiffSummaryObj, GitObject, LanguageStatsObj, LogOptionsInput,
MergeStatusObj, ObjectFormatEnum, PackStatsObj, PageInput, PatchObj, RefObj, RepoStatsObj,
RepositoryObj, SubmoduleObj, TagObj, resolve_git_object,
};
const DEFAULT_FIRST: i32 = 30;
const MAX_FIRST: i32 = 100;
#[derive(Clone)]
pub struct GitQuery {
pub(crate) repo: GitRepository,
}
impl GitQuery {
pub fn new(repo: GitRepository) -> Self {
Self { repo }
}
}
fn clamp_first(first: Option<i32>) -> i32 {
let v = first.unwrap_or(DEFAULT_FIRST).max(0);
v.min(MAX_FIRST)
}
fn parse_oid(hex: &str) -> Result<gix::hash::ObjectId> {
gix::hash::ObjectId::from_hex(hex.as_bytes())
.map_err(|e| async_graphql::Error::new(format!("invalid object id `{hex}`: {e}")))
}
fn parse_cursor(cursor: Option<String>) -> Result<usize> {
let Some(c) = cursor else { return Ok(0) };
c.parse::<usize>()
.map_err(|e| async_graphql::Error::new(format!("invalid cursor `{c}`: {e}")))
}
pub fn inject_repo<'a>(ctx: &'a Context<'_>, _repo: &'a GitRepository) {
let _ = ctx;
}
#[Object]
impl GitQuery {
async fn api_version(&self) -> &'static str {
"1.0.0"
}
async fn repository(&self) -> Result<RepositoryObj> {
let format = crate::object::impls::ObjectOps::object_format(&self.repo)?;
let stats = crate::gc::impls::GcOps::gc_stats(&self.repo)?;
Ok(RepositoryObj {
object_format: ObjectFormatEnum::from(format),
stats: RepoStatsObj::from(&stats),
})
}
async fn object_format(&self) -> Result<ObjectFormatEnum> {
let f = crate::object::impls::ObjectOps::object_format(&self.repo)?;
Ok(ObjectFormatEnum::from(f))
}
async fn gc_stats(&self) -> Result<RepoStatsObj> {
let s = crate::gc::impls::GcOps::gc_stats(&self.repo)?;
Ok(RepoStatsObj::from(&s))
}
async fn pack_stats(&self) -> Result<PackStatsObj> {
let s = crate::gc::impls::GcOps::gc_count_objects(&self.repo)?;
Ok(PackStatsObj::from(&s))
}
async fn object(&self, oid: String) -> Result<GitObject> {
let id = parse_oid(&oid)?;
resolve_git_object(&self.repo, id).await
}
async fn commit(&self, oid: Option<String>, rev: Option<String>) -> Result<CommitObj> {
let c = if let Some(hex) = oid {
let id = parse_oid(&hex)?;
crate::commit::impls::CommitOps::commit_by_id(&self.repo, &id)?
} else if let Some(spec) = rev {
crate::commit::impls::CommitOps::commit_commit(&self.repo, &spec)?
} else {
return Err(async_graphql::Error::new(
"either `oid` or `rev` must be provided",
));
};
Ok(CommitObj::from_commit(&c))
}
async fn blob(&self, oid: String) -> Result<BlobObj> {
let id = parse_oid(&oid)?;
let b = crate::blob::impls::BlobOps::blob_content(&self.repo, &id)?;
Ok(BlobObj::from_content(&b))
}
async fn tree(&self, oid: String) -> Result<super::types::TreeObj> {
let id = parse_oid(&oid)?;
let t = crate::tree::impls::TreeOps::tree_read(&self.repo, &id)?;
Ok(super::types::TreeObj::from(&t))
}
async fn tag(&self, name: String) -> Result<TagObj> {
let t = crate::tag::impls::TagOps::tag_get(&self.repo, &name)?;
Ok(TagObj::from(&t))
}
async fn ref_(&self, name: String) -> Result<RefObj> {
let info = crate::refs::impls::RefsOps::refs_get(&self.repo, &name)?;
Ok(RefObj::from_info(&info, &self.repo))
}
async fn refs(&self, query: Option<String>, page: Option<PageInput>) -> Result<Vec<RefObj>> {
let pattern = query.unwrap_or_default();
let all = crate::refs::impls::RefsOps::refs_list_filtered(&self.repo, &pattern)?;
let first = clamp_first(page.as_ref().and_then(|p| p.first));
let offset = parse_cursor(page.as_ref().and_then(|p| p.after.clone()))?;
let slice = all.into_iter().skip(offset).take(first as usize);
Ok(slice
.map(|info| RefObj::from_info(&info, &self.repo))
.collect())
}
async fn branches(&self, page: Option<PageInput>) -> Result<Vec<RefObj>> {
let first = clamp_first(page.as_ref().and_then(|p| p.first));
let offset = parse_cursor(page.as_ref().and_then(|p| p.after.clone()))?;
let all = crate::refs::impls::RefsOps::refs_list(&self.repo)?;
let branches: Vec<_> = all
.into_iter()
.filter(|r| matches!(r.r#type, crate::refs::types::RefType::Branch))
.skip(offset)
.take(first as usize)
.collect();
Ok(branches
.iter()
.map(|info| RefObj::from_info(info, &self.repo))
.collect())
}
async fn tags(&self, page: Option<PageInput>) -> Result<Vec<TagObj>> {
let first = clamp_first(page.as_ref().and_then(|p| p.first));
let offset = parse_cursor(page.as_ref().and_then(|p| p.after.clone()))?;
let tag_list = crate::tag::impls::TagOps::tag_list(&self.repo, 0, u64::MAX)?;
let slice = tag_list.tags.into_iter().skip(offset).take(first as usize);
Ok(slice.map(|t| TagObj::from(&t)).collect())
}
async fn log(&self, opts: Option<LogOptionsInput>) -> Result<Vec<CommitObj>> {
let opts = opts.unwrap_or_default();
let first = clamp_first(opts.first);
if opts.rev.is_none() || opts.rev.as_deref() == Some("HEAD") {
let all = crate::commit::impls::CommitOps::commit_list(&self.repo, first as u64, 0)?;
return Ok(all.commits.iter().map(CommitObj::from_commit).collect());
}
let log_opts = crate::log::types::LogOptions {
limit: Some(first as u64),
skip: Some(parse_cursor(opts.after.clone())? as u64),
cursor: opts.after,
first_parent: opts.first_parent.unwrap_or(false),
};
let list = crate::log::impls::LogOps::log_list(&self.repo, &log_opts)?;
Ok(list.commits.iter().map(CommitObj::from_commit).collect())
}
async fn diff(&self, base: String, head: String) -> Result<DiffObj> {
let d = crate::diff::impls::DiffOps::diff_diff(&self.repo, &base, &head)?;
Ok(DiffObj::from(&d))
}
async fn diff_short_stat(&self, base: String, head: String) -> Result<DiffSummaryObj> {
let s = crate::diff::impls::DiffOps::diff_short_stat(&self.repo, &base, &head)?;
Ok(DiffSummaryObj::from(&s))
}
async fn diff_files_changed(&self, base: String, head: String) -> Result<Vec<String>> {
crate::diff::impls::DiffOps::diff_files_changed_between(&self.repo, &base, &head)
.map_err(Into::into)
}
async fn comparison(&self, base_ref: String, head_ref: String) -> Result<ComparisonObj> {
let stat = crate::diff::impls::DiffOps::diff_short_stat(&self.repo, &base_ref, &head_ref)?;
let log =
crate::log::impls::LogOps::log_commits_between(&self.repo, &base_ref, &head_ref, 256)?;
let status = if stat.total_addition == 0 && stat.total_deletion == 0 {
ComparisonStatus::Identical
} else if stat.total_addition > 0 && stat.total_deletion == 0 {
ComparisonStatus::Ahead
} else if stat.total_addition == 0 {
ComparisonStatus::Behind
} else {
ComparisonStatus::Diverged
};
Ok(ComparisonObj {
base_ref,
head_ref,
ahead: stat.total_addition,
behind: stat.total_deletion,
status,
num_files: stat.num_files,
commits: log.commits.iter().map(CommitObj::from_commit).collect(),
})
}
async fn blame(&self, oid: String, path: String) -> Result<BlameObj> {
let rev = parse_oid(&oid)?;
let pathbuf = std::path::PathBuf::from(&path);
let hunks = blame_porcelain(&self.repo, rev, &pathbuf)?;
let total_lines = hunks.iter().map(|h| h.ending_line).max().unwrap_or(0);
let ranges = hunks
.into_iter()
.map(|h| BlameRangeObj {
starting_line: h.starting_line,
ending_line: h.ending_line,
age: 0,
path: h.path,
commit_oid: super::scalars::ObjectIdScalar(h.commit_oid),
abbreviated_oid: h.abbreviated_oid,
})
.collect();
Ok(BlameObj {
ranges,
total_lines,
})
}
async fn languages(&self) -> Result<LanguageStatsObj> {
let s = crate::analysis::impls::AnalysisOps::analysis_languages(&self.repo)?;
Ok(LanguageStatsObj::from(&s))
}
async fn contributors(&self) -> Result<Vec<ContributorStatObj>> {
let v = crate::analysis::impls::AnalysisOps::analysis_contributors(&self.repo)?;
Ok(v.iter().map(ContributorStatObj::from).collect())
}
async fn submodules(&self, oid: String) -> Result<Vec<SubmoduleObj>> {
let id = parse_oid(&oid)?;
let list = crate::submodule::impls::SubmoduleOps::submodule_list(&self.repo, &id)?;
Ok(list.submodules.iter().map(SubmoduleObj::from).collect())
}
async fn conflict_check(&self, base: String, head: String) -> Result<MergeStatusObj> {
let m = crate::conflict::impls::ConflictOps::conflict_check(&self.repo, &base, &head)?;
Ok(MergeStatusObj::from(&m))
}
async fn patch_generate(&self, base: String, head: String) -> Result<PatchObj> {
let p = crate::patch::impls::PatchOps::patch_generate(&self.repo, &base, &head)?;
Ok(PatchObj::from(&p))
}
}
#[derive(Debug, Clone)]
pub(crate) struct BlameHunk {
pub starting_line: i32,
pub ending_line: i32,
pub commit_oid: gix::hash::ObjectId,
pub abbreviated_oid: String,
pub path: String,
}
fn blame_porcelain(
repo: &crate::GitRepository,
rev: gix::hash::ObjectId,
path: &std::path::Path,
) -> Result<Vec<BlameHunk>> {
use std::collections::BTreeMap;
let rev_hex = rev.to_hex().to_string();
let rel_path = path.to_string_lossy().to_string();
let out = crate::gitcli::run(
&repo.path,
&["blame", "--porcelain", &rev_hex, "--", &rel_path],
)
.map_err(|e| async_graphql::Error::new(format!("git blame failed: {e}")))?;
let mut current_sha: Option<gix::hash::ObjectId> = None;
let mut final_line = 0_i32;
let mut in_header = true;
let mut last_path_per_sha: BTreeMap<gix::hash::ObjectId, String> = BTreeMap::new();
let mut line_for_sha: BTreeMap<gix::hash::ObjectId, i32> = BTreeMap::new();
for raw_line in out.lines() {
if raw_line.is_empty() {
continue;
}
if in_header {
let mut parts = raw_line.split_whitespace();
let sha = parts.next().unwrap_or("");
if let Some(final_l) = parts.nth(1) {
if let Ok(n) = final_l.parse::<i32>() {
final_line = n;
}
}
if let Ok(id) = gix::hash::ObjectId::from_hex(sha.as_bytes()) {
current_sha = Some(id);
line_for_sha.entry(id).or_insert(final_line);
}
in_header = false;
continue;
}
if let Some(rest) = raw_line.strip_prefix("filename ") {
if let Some(sha) = current_sha {
last_path_per_sha.insert(sha, rest.trim().to_string());
}
continue;
}
if raw_line.starts_with('\t') {
in_header = true;
}
}
let mut entries: Vec<(gix::hash::ObjectId, i32, String)> = line_for_sha
.into_iter()
.map(|(sha, line)| {
let path = last_path_per_sha
.get(&sha)
.cloned()
.unwrap_or_else(|| rel_path.clone());
(sha, line, path)
})
.collect();
entries.sort_by_key(|(_, line, _)| *line);
let mut hunks = Vec::with_capacity(entries.len());
for (sha, line, path) in entries {
let hex = sha.to_hex().to_string();
hunks.push(BlameHunk {
starting_line: line,
ending_line: line,
commit_oid: sha,
abbreviated_oid: hex.get(..7).unwrap_or("").to_string(),
path,
});
}
Ok(hunks)
}