use std::path::{Path, PathBuf}; use proto::client::Intercepted; use proto::kenshell::common::v1::PageRequest; use proto::kenshell::v1 as pb; use proto::kenshell::v1::commit_service_client::CommitServiceClient; use proto::kenshell::v1::diff_service_client::DiffServiceClient; use proto::kenshell::v1::ref_service_client::RefServiceClient; use proto::kenshell::v1::tree_service_client::TreeServiceClient; use crate::rpcgit::GitBaby; use crate::rpcgit::archive::types::{ ArchiveFormat, BundleRequest, CreateArchiveRequest, GetArchiveRequest, }; use crate::rpcgit::blame::types::{BlameHunk, BlameOptions}; use crate::rpcgit::blob::types::{ BlobContent, BlobInfo, GetBlobOptions, LFSPointer, ListLFSPointersOptions, }; use crate::rpcgit::branch::types::{BranchInfo, ListBranchesOptions}; use crate::rpcgit::commit::types::{ CommitInfo, CommitShortStat, CommitStats, ListCommitsOptions, Signature, TreeEntry, TreeEntryMode, }; use crate::rpcgit::conflict::types::ListConflictOptions; use crate::rpcgit::diff::types::{ ChangedPath, ChangedPathDiff, ChangedPathRequest, DiffOptions, DiffRequest, DiffStatus, NumStat, RangeDiffSpec, ShortStat, }; use crate::rpcgit::error::BabyError; use crate::rpcgit::oid::ObjectId; use crate::rpcgit::refs::types::{ListRefsOptions, RefInfo, RefType}; use crate::rpcgit::setup::types::ObjectFormat; use crate::rpcgit::tags::types::{ CreateTagOptions, ListTagsOptions, TagInfo, TagType, TagVerifyResult, }; use crate::rpcgit::tree::types::{ DiffTreesOptions, TreeDiff, TreeDiffStatus, TreeEntryWithLatest, WalkTreeOptions, }; fn unsupported(what: &str) -> BabyError { BabyError::Custom(format!("{what} is not provided by KenShell")) } fn oid(text: &str) -> Result { ObjectId::parse(text).map_err(|_| BabyError::RevisionInvalid(text.to_string())) } fn oid_opt(text: &str) -> Option { if text.trim().is_empty() { None } else { ObjectId::parse(text).ok() } } fn signature(person: Option) -> Signature { let person = person.unwrap_or_default(); Signature { name: person.name, email: person.email, time: person .at .and_then(|at| time::OffsetDateTime::from_unix_timestamp(at.seconds).ok()) .unwrap_or(time::OffsetDateTime::UNIX_EPOCH), } } /// 仓库概览,字段全部来自 KenShell。 #[derive(Debug, Clone, Default)] pub struct RepositoryOverviewInfo { pub default_branch: String, pub primary_language: Option, pub object_format: String, pub size_bytes: i64, pub branch_count: i32, pub tag_count: i32, } fn mode_of(text: &str) -> TreeEntryMode { TreeEntryMode::from_octal_str(text).unwrap_or(TreeEntryMode::Blob) } /// `google.protobuf.Timestamp` → `OffsetDateTime`;非法值当作缺失。 fn at(value: Option) -> Option { value.and_then(|at| time::OffsetDateTime::from_unix_timestamp(at.seconds).ok()) } fn entry_from_view(view: pb::TreeEntryView) -> TreeEntry { let name = if view.name_bytes.is_empty() { view.name } else { String::from_utf8_lossy(&view.name_bytes).into_owned() }; let mode = match pb::TreeEntryType::try_from(view.r#type).unwrap_or(pb::TreeEntryType::Unspecified) { pb::TreeEntryType::Submodule => TreeEntryMode::Submodule, pb::TreeEntryType::Symlink => TreeEntryMode::Symlink, _ => mode_of(&view.mode), }; TreeEntry { oid: oid(&view.oid).unwrap_or_else(|_| { oid_opt("4b825dc642cb6eb9a060e54bf8d69288fbee4904").expect("empty tree") }), name, mode, size: Some(view.size_bytes), } } fn entry_from_view_path(view: pb::TreeEntryView) -> TreeEntry { let mut entry = entry_from_view(view.clone()); if !view.path.is_empty() { entry.name = view.path; } entry } fn commit_from_view(view: pb::CommitView) -> CommitInfo { CommitInfo { id: oid(&view.id) .unwrap_or_else(|_| oid_opt("0000000000000000000000000000000000000000").expect("zero")), tree_id: oid(&view.tree_id).unwrap_or_else(|_| { oid_opt("4b825dc642cb6eb9a060e54bf8d69288fbee4904").expect("empty tree") }), parents: view .parent_ids .iter() .filter_map(|p| ObjectId::parse(p).ok()) .collect(), author: signature(view.author), committer: signature(view.committer), message: view.message, gpg_signature: None, } } fn ref_type(value: i32) -> RefType { match pb::RefType::try_from(value).unwrap_or(pb::RefType::Unspecified) { pb::RefType::Branch => RefType::Branch, pb::RefType::Tag => RefType::Tag, pb::RefType::RemoteBranch => RefType::Remote, _ => RefType::Other, } } fn path_or_empty(path: Option) -> String { path.map(|p| p.to_string_lossy().into_owned()) .unwrap_or_default() } fn page_request(page_size: u32, token: String) -> PageRequest { PageRequest { page_size, page_token: token, } } fn changed_path(view: pb::ChangedPathView) -> ChangedPath { ChangedPath { old_path: if view.old_path.is_empty() { None } else { Some(PathBuf::from(view.old_path)) }, new_path: PathBuf::from(view.new_path), old_mode: view.old_mode, new_mode: view.new_mode, old_oid: oid_opt(&view.old_oid), new_oid: oid_opt(&view.new_oid), status: { let __inline = |text: &str| -> DiffStatus { match text { "A" | "added" | "Added" => DiffStatus::Added, "D" | "deleted" | "Deleted" => DiffStatus::Deleted, "T" | "type_changed" | "TypeChanged" => DiffStatus::TypeChanged, "R" | "renamed" | "Renamed" => DiffStatus::Renamed, "C" | "copied" | "Copied" => DiffStatus::Copied, _ => DiffStatus::Modified, } }; __inline(&view.status) }, } } impl GitBaby { fn common_ref(&self) -> proto::kenshell::common::v1::RepositoryRef { self.context().repository_ref() } fn refs_client(&self) -> RefServiceClient { self.context().ks.refs() } fn commit_client(&self) -> CommitServiceClient { self.context().ks.commit() } fn tree_client(&self) -> TreeServiceClient { self.context().ks.tree() } fn diff_client(&self) -> DiffServiceClient { self.context().ks.diff() } fn repository_ref_some(&self) -> Option { Some(self.common_ref()) } pub async fn rev_parse(&self, revision: &str) -> Result { let mut client = self.commit_client(); let response = client .rev_parse(pb::RevParseRequest { repository: self.repository_ref_some(), revision: revision.to_string(), }) .await .map_err(BabyError::from)? .into_inner(); oid(&response.oid) } pub async fn resolve_revision(&self, revision: &str) -> Result { self.rev_parse(revision).await } pub async fn is_ancestor(&self, ancestor: &str, descendant: &str) -> Result { let mut client = self.commit_client(); let response = client .is_ancestor(pb::IsAncestorRequest { repository: self.repository_ref_some(), ancestor: ancestor.to_string(), descendant: descendant.to_string(), }) .await .map_err(BabyError::from)? .into_inner(); Ok(response.is_ancestor) } pub async fn get_commit(&self, revision: &str) -> Result { let mut client = self.commit_client(); let response = client .get_commit(pb::GetCommitRequest { repository: self.repository_ref_some(), commit_id: revision.to_string(), }) .await .map_err(BabyError::from)? .into_inner(); response .commit .map(commit_from_view) .ok_or_else(|| BabyError::RevisionNotFound(revision.to_string())) } pub async fn list_commits( &self, opts: ListCommitsOptions, ) -> Result, BabyError> { let limit = opts.max_count.unwrap_or(50).max(1); let skip = opts.skip.unwrap_or(0); let mut out = Vec::new(); let mut token = String::new(); let mut seen = 0u32; loop { let mut client = self.commit_client(); let response = client .list_commits(pb::ListCommitsRequest { repository: self.repository_ref_some(), revision: opts.revision.clone(), path: path_or_empty(opts.path.clone()), query: String::new(), first_parent: opts.first_parent, page: Some(page_request(limit.min(200), token.clone())), since: opts.since.clone().unwrap_or_default(), until: opts.until.clone().unwrap_or_default(), }) .await .map_err(BabyError::from)? .into_inner(); for commit in response.commits { if seen >= skip { out.push(commit_from_view(commit)); } seen += 1; } let next = response .page .as_ref() .map(|p| p.next_page_token.clone()) .unwrap_or_default(); if out.len() >= limit as usize || next.is_empty() { break; } token = next; } out.truncate(limit as usize); Ok(out) } pub async fn count_commits(&self, opts: ListCommitsOptions) -> Result { let mut client = self.commit_client(); let response = client .list_commits(pb::ListCommitsRequest { repository: self.repository_ref_some(), revision: opts.revision, path: path_or_empty(opts.path), query: String::new(), first_parent: opts.first_parent, page: Some(page_request(1, String::new())), since: opts.since.unwrap_or_default(), until: opts.until.unwrap_or_default(), }) .await .map_err(BabyError::from)? .into_inner(); Ok(response .total .or_else(|| response.page.as_ref().and_then(|p| p.total_size)) .unwrap_or(0)) } pub async fn commit_stats( &self, revision: &str, path: Option<&Path>, ) -> Result { let mut client = self.commit_client(); let response = client .get_commit_stats(pb::GetCommitStatsRequest { repository: self.repository_ref_some(), commit_id: revision.to_string(), path: path .map(|p| p.to_string_lossy().into_owned()) .unwrap_or_default(), }) .await .map_err(BabyError::from)? .into_inner(); let stats = response.stats.unwrap_or_default(); Ok(CommitStats { additions: stats.additions as u64, deletions: stats.deletions as u64, }) } pub async fn commit_short_stats( &self, revision: &str, ) -> Result, BabyError> { let commits = self .list_commits(ListCommitsOptions { revision: revision.to_string(), max_count: Some(200), ..Default::default() }) .await?; let mut out = Vec::with_capacity(commits.len()); for commit in commits { let stats = self .commit_stats(&commit.id.to_string(), None) .await .unwrap_or_default(); out.push(CommitShortStat { oid: commit.id, author_name: commit.author.name, author_email: commit.author.email, additions: stats.additions, deletions: stats.deletions, }); } Ok(out) } pub async fn blame_all(&self, opts: BlameOptions) -> Result, BabyError> { let mut client = self.commit_client(); let mut token = String::new(); let mut hunks: Vec = Vec::new(); loop { let response = client .list_blame(pb::ListBlameRequest { repository: self.repository_ref_some(), revision: opts.revision.clone(), path: opts.path.to_string_lossy().into_owned(), page: Some(page_request(500, token.clone())), }) .await .map_err(BabyError::from)? .into_inner(); for line in response.lines { let sha = ObjectId::parse(&line.commit_id).unwrap_or_else(|_| { ObjectId::parse("0000000000000000000000000000000000000000").expect("zero") }); match hunks.last_mut() { Some(last) if last.sha == sha => { last.lines.push(line.content); last.final_line_count += 1; } _ => hunks.push(BlameHunk { sha, previous_sha: None, previous_path: None, lines: vec![line.content], final_start_line: line.line_number, final_line_count: 1, orig_start_line: 0, }), } } let next = response .page .as_ref() .map(|p| p.next_page_token.clone()) .unwrap_or_default(); if next.is_empty() { break; } token = next; } Ok(hunks) } pub async fn get_tree_entries( &self, revision: &str, path: &Path, ) -> Result, BabyError> { let mut client = self.tree_client(); let response = client .get_tree(pb::GetTreeRequest { repository: self.repository_ref_some(), revision: revision.to_string(), path: path.to_string_lossy().into_owned(), page: None, path_bytes: Vec::new(), }) .await .map_err(BabyError::from)? .into_inner(); Ok(response.entries.into_iter().map(entry_from_view).collect()) } pub async fn tree_entry_at( &self, revision: &str, path: &Path, ) -> Result, BabyError> { let mut client = self.tree_client(); let response = client .resolve_tree_path(pb::ResolveTreePathRequest { repository: self.repository_ref_some(), revision: revision.to_string(), path: path.to_string_lossy().into_owned(), path_bytes: Vec::new(), }) .await; match response { Ok(response) => Ok(response.into_inner().entry.map(entry_from_view)), Err(status) if status.code() == tonic::Code::NotFound => Ok(None), Err(status) => Err(BabyError::from(status)), } } pub async fn walk_tree( &self, revision: &str, path: &Path, opts: WalkTreeOptions, ) -> Result, BabyError> { let mut client = self.tree_client(); let mut token = String::new(); let mut out = Vec::new(); loop { let response = client .list_files(pb::ListFilesRequest { repository: self.repository_ref_some(), revision: revision.to_string(), path: path.to_string_lossy().into_owned(), recursive: opts.recurse, page: Some(page_request(200, token.clone())), include_directories: opts.show_trees, path_bytes: Vec::new(), }) .await .map_err(BabyError::from)? .into_inner(); out.extend(response.files.into_iter().map(entry_from_view_path)); let next = response .page .as_ref() .map(|p| p.next_page_token.clone()) .unwrap_or_default(); if next.is_empty() { break; } token = next; } Ok(out) } pub async fn list_dir( &self, revision: &str, path: Option<&str>, ) -> Result, BabyError> { let mut client = self.tree_client(); let response = client .get_tree(pb::GetTreeRequest { repository: self.repository_ref_some(), revision: revision.to_string(), path: path.unwrap_or("").to_string(), page: None, path_bytes: Vec::new(), }) .await .map_err(BabyError::from)? .into_inner(); Ok(response.entries.into_iter().map(entry_from_view).collect()) } async fn entries_with_latest( &self, revision: &str, path: Option<&str>, recursive: bool, ) -> Result, BabyError> { let mut client = self.tree_client(); let mut token = String::new(); let mut out = Vec::new(); loop { let response = client .get_tree(pb::GetTreeRequest { repository: self.repository_ref_some(), revision: revision.to_string(), path: path.unwrap_or("").to_string(), page: Some(page_request(200, token.clone())), path_bytes: Vec::new(), }) .await .map_err(BabyError::from)? .into_inner(); for entry in response.entries { let latest = entry .latest_commit .clone() .map(commit_from_view) .unwrap_or_else(|| commit_from_view(pb::CommitView::default())); out.push(TreeEntryWithLatest { entry: entry_from_view(entry), latest, }); } let next = response .page .as_ref() .map(|p| p.next_page_token.clone()) .unwrap_or_default(); if next.is_empty() { break; } token = next; } if recursive { let mut full = Vec::new(); for item in out { if item.entry.mode == TreeEntryMode::Tree { continue; } full.push(item); } return Ok(full); } Ok(out) } pub async fn tree_entries_with_latest( &self, revision: &str, path: Option<&str>, ) -> Result, BabyError> { self.entries_with_latest(revision, path, false).await } pub async fn tree_dir_entries_with_latest( &self, revision: &str, path: Option<&str>, ) -> Result, BabyError> { self.entries_with_latest(revision, path, false).await } pub async fn get_blob( &self, oid_value: ObjectId, opts: GetBlobOptions, ) -> Result { let mut client = self.context().ks.blob(); let response = client .get_blob_content(pb::GetBlobContentRequest { repository: self.repository_ref_some(), oid: oid_value.to_string(), max_bytes: opts.limit.unwrap_or(0).max(0) as u64, }) .await .map_err(BabyError::from)? .into_inner(); let content = response.content.unwrap_or_default(); let size = content.size; Ok(BlobContent { info: BlobInfo { oid: oid_opt(&content.oid).unwrap_or(oid_value), size, data: Some(content.data.clone()), }, data: content.data, }) } pub async fn get_blob_size(&self, oid_value: ObjectId) -> Result { let mut client = self.context().ks.blob(); let response = client .get_blob_metadata(pb::GetBlobMetadataRequest { repository: self.repository_ref_some(), oid: oid_value.to_string(), }) .await .map_err(BabyError::from)? .into_inner(); Ok(response.metadata.map(|m| m.size).unwrap_or(0)) } pub async fn list_lfs_pointers( &self, opts: ListLFSPointersOptions, ) -> Result, BabyError> { let mut client = self.context().ks.blob(); let response = client .list_lfs_pointers(pb::ListLfsPointersRequest { repository: self.repository_ref_some(), revision: opts.revisions.first().cloned().unwrap_or_default(), limit: opts.limit.unwrap_or(100), }) .await .map_err(BabyError::from)? .into_inner(); Ok(response .pointers .into_iter() .map(|p| LFSPointer { oid: oid_opt(&p.oid).unwrap_or_else(|| { oid_opt("0000000000000000000000000000000000000000").expect("zero") }), size: p.size, file_oid: oid_opt("0000000000000000000000000000000000000000").expect("zero"), file_size: p.size, content: Vec::new(), }) .collect()) } pub async fn list_refs(&self, opts: ListRefsOptions) -> Result, BabyError> { let mut client = self.refs_client(); let mut out = Vec::new(); let mut token = String::new(); let prefix = opts.patterns.first().cloned().unwrap_or_default(); let limit = opts.limit.unwrap_or(500); loop { let response = client .list_refs(pb::ListRefsRequest { repository: self.repository_ref_some(), prefix: prefix.clone(), page: Some(page_request(limit.min(200), token.clone())), }) .await .map_err(BabyError::from)? .into_inner(); for item in response.refs { out.push(RefInfo { name: item.name, ref_type: ref_type(item.r#type), sha: oid(&item.sha).unwrap_or_else(|_| { oid_opt("0000000000000000000000000000000000000000").expect("zero") }), peeled: None, }); } let next = response .page .as_ref() .map(|p| p.next_page_token.clone()) .unwrap_or_default(); if next.is_empty() || out.len() >= limit as usize { break; } token = next; } if !opts.patterns.is_empty() { out.retain(|r| opts.patterns.iter().any(|p| r.name.contains(p.as_str()))); } if !opts.exclude_patterns.is_empty() { out.retain(|r| { !opts .exclude_patterns .iter() .any(|p| r.name.contains(p.as_str())) }); } Ok(out) } pub async fn find_refs_by_oid(&self, target: ObjectId) -> Result, BabyError> { let refs = self.list_refs(ListRefsOptions::default()).await?; Ok(refs.into_iter().filter(|r| r.sha == target).collect()) } pub async fn update_ref( &self, name: &str, new_sha: ObjectId, old_sha: Option, ) -> Result<(), BabyError> { let branch = name .strip_prefix("refs/heads/") .ok_or_else(|| unsupported("updating non-branch refs"))?; let mut client = self.context().ks.command(); client .update_branch(pb::UpdateBranchRequest { repository: self.repository_ref_some(), name: branch.to_string(), new_sha: new_sha.to_string(), expected_old_sha: old_sha.map(|o| o.to_string()).unwrap_or_default(), actor: Some(self.context().actor()), options: Some(self.context().write_options()), }) .await .map_err(BabyError::from)?; Ok(()) } pub async fn delete_ref(&self, name: &str) -> Result<(), BabyError> { let branch = name .strip_prefix("refs/heads/") .ok_or_else(|| unsupported("deleting non-branch refs"))?; let mut client = self.context().ks.command(); client .delete_branch(pb::DeleteBranchRequest { repository: self.repository_ref_some(), name: branch.to_string(), expected_sha: String::new(), actor: Some(self.context().actor()), options: Some(self.context().write_options()), force: true, }) .await .map_err(BabyError::from)?; Ok(()) } pub async fn set_default_branch(&self, name: &str) -> Result<(), BabyError> { let mut client = self.context().ks.command(); client .set_default_branch(pb::SetDefaultBranchRequest { repository: self.repository_ref_some(), name: name.to_string(), actor: Some(self.context().actor()), options: Some(self.context().write_options()), }) .await .map_err(BabyError::from)?; Ok(()) } pub async fn get_default_branch(&self) -> Result { let mut client = self.refs_client(); let response = client .get_default_branch(pb::GetDefaultBranchRequest { repository: self.repository_ref_some(), }) .await .map_err(BabyError::from)? .into_inner(); if response.name.is_empty() { Err(BabyError::RevisionNotFound("HEAD".into())) } else { Ok(response.name) } } pub async fn list_branches( &self, opts: ListBranchesOptions, ) -> Result, BabyError> { let mut client = self.refs_client(); let mut token = String::new(); let mut out = Vec::new(); loop { let response = client .list_branches(pb::ListBranchesRequest { repository: self.repository_ref_some(), query: opts.patterns.first().cloned().unwrap_or_default(), sort: pb::BranchSort::NameAsc as i32, page: Some(page_request(200, token.clone())), }) .await .map_err(BabyError::from)? .into_inner(); for branch in response.branches { out.push(BranchInfo { name: branch.name, sha: oid(&branch.sha).unwrap_or_else(|_| { oid_opt("0000000000000000000000000000000000000000").expect("zero") }), is_protected: branch.is_protected, is_deleted: branch.is_deleted, deleted_at: at(branch.deleted_at), created_at: at(branch.created_at), updated_at: at(branch.updated_at), }); } let next = response .page .as_ref() .map(|p| p.next_page_token.clone()) .unwrap_or_default(); if next.is_empty() { break; } token = next; } Ok(out) } pub async fn find_branch(&self, name: &str) -> Result { let mut client = self.refs_client(); let response = client .get_branch(pb::GetBranchRequest { repository: self.repository_ref_some(), name: name.to_string(), }) .await .map_err(BabyError::from)? .into_inner(); let branch = response .branch .ok_or_else(|| BabyError::BranchNotFound(name.to_string()))?; Ok(BranchInfo { name: branch.name, sha: oid(&branch.sha).unwrap_or_else(|_| { oid_opt("0000000000000000000000000000000000000000").expect("zero") }), is_protected: branch.is_protected, is_deleted: branch.is_deleted, deleted_at: at(branch.deleted_at), created_at: at(branch.created_at), updated_at: at(branch.updated_at), }) } pub async fn create_branch( &self, name: &str, start_point: Option<&str>, ) -> Result<(), BabyError> { let mut client = self.context().ks.command(); client .create_branch(pb::CreateBranchRequest { repository: self.repository_ref_some(), name: name.to_string(), start_point: start_point.unwrap_or("").to_string(), actor: Some(self.context().actor()), options: Some(self.context().write_options()), }) .await .map_err(BabyError::from)?; Ok(()) } pub async fn delete_branch(&self, name: &str, force: bool) -> Result<(), BabyError> { let mut client = self.context().ks.command(); client .delete_branch(pb::DeleteBranchRequest { repository: self.repository_ref_some(), name: name.to_string(), expected_sha: String::new(), actor: Some(self.context().actor()), options: Some(self.context().write_options()), force, }) .await .map_err(BabyError::from)?; Ok(()) } pub async fn rename_branch(&self, from: &str, to: &str) -> Result<(), BabyError> { let mut client = self.context().ks.command(); client .rename_branch(pb::RenameBranchRequest { repository: self.repository_ref_some(), name: from.to_string(), new_name: to.to_string(), actor: Some(self.context().actor()), options: Some(self.context().write_options()), }) .await .map_err(BabyError::from)?; Ok(()) } pub async fn list_tags(&self, opts: ListTagsOptions) -> Result, BabyError> { let mut client = self.refs_client(); let mut token = String::new(); let mut out = Vec::new(); loop { let response = client .list_tags(pb::ListTagsRequest { repository: self.repository_ref_some(), query: opts.patterns.first().cloned().unwrap_or_default(), sort: pb::TagSort::NameAsc as i32, page: Some(page_request(200, token.clone())), }) .await .map_err(BabyError::from)? .into_inner(); for tag in response.tags { out.push(tag_from_view(tag)); } let next = response .page .as_ref() .map(|p| p.next_page_token.clone()) .unwrap_or_default(); if next.is_empty() { break; } token = next; } Ok(out) } pub async fn find_tag(&self, name: &str) -> Result { let mut client = self.refs_client(); let response = client .get_tag(pb::GetTagRequest { repository: self.repository_ref_some(), name: name.to_string(), }) .await .map_err(BabyError::from)? .into_inner(); response .tag .map(tag_from_view) .ok_or_else(|| BabyError::TagNotFound(name.to_string())) } pub async fn create_lightweight_tag(&self, opts: CreateTagOptions) -> Result<(), BabyError> { self.create_tag(opts, false).await } pub async fn create_annotated_tag(&self, opts: CreateTagOptions) -> Result<(), BabyError> { self.create_tag(opts, true).await } async fn create_tag(&self, opts: CreateTagOptions, annotated: bool) -> Result<(), BabyError> { let mut client = self.context().ks.command(); client .create_tag(pb::CreateTagRequest { repository: self.repository_ref_some(), name: opts.name, target_sha: opts.target, message: opts.message.unwrap_or_default(), actor: Some(self.context().actor()), options: Some(self.context().write_options()), annotated, }) .await .map_err(BabyError::from)?; Ok(()) } pub async fn delete_tag(&self, name: &str) -> Result<(), BabyError> { let mut client = self.context().ks.command(); client .delete_tag(pb::DeleteTagRequest { repository: self.repository_ref_some(), name: name.to_string(), expected_sha: String::new(), actor: Some(self.context().actor()), options: Some(self.context().write_options()), }) .await .map_err(BabyError::from)?; Ok(()) } pub async fn raw_diff( &self, req: &DiffRequest, _opts: Option<&DiffOptions>, ) -> Result { let mut client = self.diff_client(); let mut stream = client .get_raw_diff(pb::GetRawDiffRequest { repository: self.repository_ref_some(), base_revision: req.left.to_string(), head_revision: req.right.to_string(), }) .await .map_err(BabyError::from)? .into_inner(); let mut out = Vec::new(); while let Some(chunk) = stream.message().await.map_err(BabyError::from)? { out.extend_from_slice(&chunk.data); } Ok(String::from_utf8_lossy(&out).into_owned()) } pub async fn range_diff( &self, spec: &RangeDiffSpec, _opts: &DiffOptions, _hex_len: Option, ) -> Result { let (from, to, base) = match spec { RangeDiffSpec::Range(a, b) => (a.clone(), b.clone(), String::new()), RangeDiffSpec::RevisionRange(a, b) => (a.clone(), b.clone(), String::new()), RangeDiffSpec::BaseWithRevisions(base, a, b) => (a.clone(), b.clone(), base.clone()), }; let mut client = self.diff_client(); let mut stream = client .get_range_diff(pb::GetRangeDiffRequest { repository: self.repository_ref_some(), from, to, base, }) .await .map_err(BabyError::from)? .into_inner(); let mut out = Vec::new(); while let Some(chunk) = stream.message().await.map_err(BabyError::from)? { out.extend_from_slice(&chunk.data); } Ok(String::from_utf8_lossy(&out).into_owned()) } pub async fn shortstat( &self, req: &DiffRequest, _opts: &DiffOptions, ) -> Result { let mut client = self.diff_client(); let response = client .get_diff_short_stat(pb::GetDiffShortStatRequest { repository: self.repository_ref_some(), base_revision: req.left.to_string(), head_revision: req.right.to_string(), }) .await .map_err(BabyError::from)? .into_inner(); let stats = response.stats.unwrap_or_default(); Ok(ShortStat { files_changed: stats.files_changed, additions: stats.additions, deletions: stats.deletions, }) } pub async fn diff_stats( &self, req: &DiffRequest, opts: &DiffOptions, ) -> Result, BabyError> { let mut client = self.diff_client(); let response = client .list_diff_num_stats(pb::ListDiffNumStatsRequest { repository: self.repository_ref_some(), base_revision: req.left.to_string(), head_revision: req.right.to_string(), paths: opts .paths .as_ref() .map(|paths| { paths .iter() .map(|p| p.to_string_lossy().into_owned()) .collect() }) .unwrap_or_default(), }) .await .map_err(BabyError::from)? .into_inner(); Ok(response .stats .into_iter() .map(|stat| NumStat { additions: stat.additions, deletions: stat.deletions, path: PathBuf::from(stat.path), old_path: None, }) .collect()) } pub async fn find_changed_paths( &self, requests: Vec, _filter_status: Option, _detect_renames: Option, _merge_parents: bool, ) -> Result, BabyError> { let mut client = self.diff_client(); let (base, head, commit) = match requests.first() { Some(ChangedPathRequest::Commit(oid_value)) => { (String::new(), String::new(), oid_value.to_string()) } Some(ChangedPathRequest::TreePair(left, right)) => { (left.to_string(), right.to_string(), String::new()) } None => return Ok(Vec::new()), }; let response = client .list_changed_paths(pb::ListChangedPathsRequest { repository: self.repository_ref_some(), base_revision: base, head_revision: head, commit_id: commit, }) .await .map_err(BabyError::from)? .into_inner(); Ok(response.paths.into_iter().map(changed_path).collect()) } pub async fn commit_diff( &self, req: &DiffRequest, _opts: &DiffOptions, _hex_len: Option, ) -> Result, BabyError> { let mut client = self.diff_client(); let response = client .compare_commits(pb::CompareCommitsRequest { repository: self.repository_ref_some(), base_revision: req.left.to_string(), head_revision: req.right.to_string(), merge_base: false, path: String::new(), page: None, }) .await .map_err(BabyError::from)? .into_inner(); Ok(response.files.into_iter().map(changed_path_diff).collect()) } pub async fn diff_trees( &self, base: &str, head: &str, _path: &Path, _opts: DiffTreesOptions, ) -> Result, BabyError> { let mut client = self.diff_client(); let response = client .list_diff_tree(pb::ListDiffTreeRequest { repository: self.repository_ref_some(), base_revision: base.to_string(), head_revision: head.to_string(), use_merge_base: false, paths: Vec::new(), }) .await .map_err(BabyError::from)? .into_inner(); Ok(response .records .into_iter() .map(|record| TreeDiff { old_path: if record.base_path.is_empty() { None } else { Some(PathBuf::from(record.base_path)) }, new_path: if record.head_path.is_empty() { None } else { Some(PathBuf::from(record.head_path)) }, old_mode: Some(mode_of(&format!("{:o}", record.base_mode))), new_mode: Some(mode_of(&format!("{:o}", record.head_mode))), old_oid: oid_opt(&record.base_blob_id), new_oid: oid_opt(&record.head_blob_id), status: { let __inline = |text: &str| -> TreeDiffStatus { match text { "A" | "added" | "Added" => TreeDiffStatus::Added, "D" | "deleted" | "Deleted" => TreeDiffStatus::Deleted, "T" | "type_changed" | "TypeChanged" => TreeDiffStatus::TypeChanged, "R" | "renamed" | "Renamed" => TreeDiffStatus::Renamed, "C" | "copied" | "Copied" => TreeDiffStatus::Copied, _ => TreeDiffStatus::Modified, } }; __inline(&record.status) }, }) .collect()) } pub async fn list_conflict_files( &self, base: &str, head: &str, opts: &ListConflictOptions, ) -> Result, BabyError> { let mut client = self.context().ks.conflict(); let response = client .check_conflicts(pb::CheckConflictsRequest { repository: self.repository_ref_some(), base_revision: base.to_string(), head_revision: head.to_string(), ancestor_revision: String::new(), }) .await .map_err(BabyError::from)? .into_inner(); let our_commit_oid = oid(base) .unwrap_or_else(|_| oid_opt("0000000000000000000000000000000000000000").expect("zero")); let mut out = Vec::new(); for path in response .status .map(|status| status.paths) .unwrap_or_default() { let content = if opts.skip_content { None } else { let mut client = self.context().ks.conflict(); client .get_conflict_detail(pb::GetConflictDetailRequest { repository: self.repository_ref_some(), base_revision: base.to_string(), head_revision: head.to_string(), path: path.clone(), }) .await .ok() .map(|response| response.into_inner().content) }; out.push(crate::rpcgit::conflict::types::ConflictFile { header: crate::rpcgit::conflict::types::ConflictFileHeader { our_commit_oid: our_commit_oid.clone(), their_path: Some(PathBuf::from(&path)), our_path: Some(PathBuf::from(&path)), ancestor_path: None, our_mode: 0, }, content, }); } Ok(out) } pub async fn commit_ids_between_reverse( &self, start: &str, end: &str, _limit: Option, max_count: Option, ) -> Result, BabyError> { let commits = self .list_commits(ListCommitsOptions { revision: format!("{start}..{end}"), max_count, ..Default::default() }) .await?; let mut ids: Vec = commits.into_iter().map(|commit| commit.id).collect(); ids.reverse(); Ok(ids) } pub async fn conflict_detail( &self, base: &str, head: &str, path: &str, ) -> Result { let mut client = self.context().ks.conflict(); let response = client .get_conflict_detail(pb::GetConflictDetailRequest { repository: self.repository_ref_some(), base_revision: base.to_string(), head_revision: head.to_string(), path: path.to_string(), }) .await .map_err(BabyError::from)? .into_inner(); Ok(response.content) } pub async fn list_remotes(&self) -> Result, BabyError> { let mut client = self.context().ks.remote(); let response = client .list_remotes(pb::ListRemotesRequest { repository: self.repository_ref_some(), }) .await .map_err(BabyError::from)? .into_inner(); Ok(response .remotes .into_iter() .map(|remote| (remote.name, remote.fetch_url)) .collect()) } pub async fn remove_remote(&self, name: &str) -> Result<(), BabyError> { let mut client = self.context().ks.remote(); client .remove_remote(pb::RemoveRemoteRequest { repository: self.repository_ref_some(), name: name.to_string(), actor: Some(self.context().actor()), options: Some(self.context().write_options()), }) .await .map_err(BabyError::from)?; Ok(()) } pub async fn create_archive(&self, req: CreateArchiveRequest) -> Result<(), BabyError> { let mut client = self.context().ks.archive(); client .start_archive(pb::StartArchiveRequest { repository: self.repository_ref_some(), revision: req.commit, format: archive_format(req.format) as i32, path_filters: req.paths, actor: Some(self.context().actor()), idempotency_key: String::new(), }) .await .map_err(BabyError::from)?; Ok(()) } pub async fn get_archive(&self, req: GetArchiveRequest) -> Result { let mut client = self.context().ks.archive(); let response = client .get_archive_artifact(pb::GetArchiveArtifactRequest { repository: self.repository_ref_some(), revision: req.commit, format: archive_format(req.format) as i32, path_filters: req.exclude, }) .await .map_err(BabyError::from)? .into_inner(); let artifact = response .artifact .ok_or_else(|| BabyError::Custom("archive artifact missing".into()))?; Ok(ArchiveArtifact { format: artifact.format, size: artifact.size_bytes, storage_key: artifact.storage_key, download_url: artifact.download_url, }) } pub async fn create_bundle(&self, _req: BundleRequest) -> Result<(), BabyError> { let mut client = self.context().ks.archive(); client .start_archive_bundle(pb::StartArchiveBundleRequest { repository: self.repository_ref_some(), actor: Some(self.context().actor()), idempotency_key: String::new(), }) .await .map_err(BabyError::from)?; Ok(()) } pub async fn init_repository( &self, _path: &str, _bare: bool, _format: Option, ) -> Result<(), BabyError> { let mut client = self.context().ks.command(); client .create_repository(pb::CreateRepositoryRequest { repository: self.repository_ref_some(), // 默认分支由托管面决定;本地不再保存该字段。 default_branch: String::new(), actor: Some(self.context().actor()), options: Some(self.context().write_options()), }) .await .map_err(BabyError::from)?; Ok(()) } /// 仓库概览:默认分支、主语言、对象格式、体积与引用计数。 /// /// 这些字段的真值都在 KenShell,本地库不再保存副本。 pub async fn repository_overview(&self) -> Result { let mut client = self.context().ks.repository(); let response = client .get_repository(pb::GetRepositoryRequest { repository: self.repository_ref_some(), }) .await .map_err(BabyError::from)? .into_inner(); let overview = response .repository .ok_or_else(|| BabyError::RefNotFound("repository".to_string()))?; let summary = overview.repository.unwrap_or_default(); let branch = summary .default_branch .strip_prefix("refs/heads/") .unwrap_or(&summary.default_branch) .to_string(); Ok(RepositoryOverviewInfo { default_branch: branch, primary_language: Some(summary.primary_language).filter(|v| !v.is_empty()), object_format: if overview.object_format.is_empty() { "sha1".to_string() } else { overview.object_format }, size_bytes: overview.size_bytes as i64, branch_count: overview.branch_count as i32, tag_count: overview.tag_count as i32, }) } /// 保护规则列表;`kind` 为 `branch` 或 `tag`。 pub async fn list_ref_protections( &self, kind: &str, ) -> Result, BabyError> { let mut client = self.refs_client(); let response = client .list_ref_protections(pb::ListRefProtectionsRequest { repository: self.repository_ref_some(), kind: if kind == "tag" { pb::RefProtectionKind::Tag as i32 } else { pb::RefProtectionKind::Branch as i32 }, }) .await .map_err(BabyError::from)? .into_inner(); Ok(response.protections) } pub async fn create_ref_protection( &self, kind: &str, pattern: &str, rules: pb::RefProtectionRules, whitelists: Vec, ) -> Result { let mut client = self.refs_client(); let response = client .create_ref_protection(pb::CreateRefProtectionRequest { repository: self.repository_ref_some(), kind: if kind == "tag" { pb::RefProtectionKind::Tag as i32 } else { pb::RefProtectionKind::Branch as i32 }, pattern: pattern.to_string(), rules: Some(rules), whitelists, }) .await .map_err(BabyError::from)? .into_inner(); response .protection .ok_or_else(|| BabyError::RefNotFound(pattern.to_string())) } pub async fn update_ref_protection( &self, id: &str, pattern: &str, rules: pb::RefProtectionRules, whitelists: Vec, ) -> Result { let mut client = self.refs_client(); let response = client .update_ref_protection(pb::UpdateRefProtectionRequest { repository: self.repository_ref_some(), id: id.to_string(), pattern: pattern.to_string(), rules: Some(rules), whitelists, }) .await .map_err(BabyError::from)? .into_inner(); response .protection .ok_or_else(|| BabyError::RefNotFound(id.to_string())) } pub async fn delete_ref_protection(&self, id: &str) -> Result<(), BabyError> { let mut client = self.refs_client(); client .delete_ref_protection(pb::DeleteRefProtectionRequest { repository: self.repository_ref_some(), id: id.to_string(), }) .await .map_err(BabyError::from)?; Ok(()) } pub async fn list_code_comments( &self, commit_sha: &str, path: &str, resolved: Option, ) -> Result, BabyError> { let mut client = self.context().ks.comment(); let response = client .list_code_comments(pb::ListCodeCommentsRequest { repository: self.repository_ref_some(), commit_sha: commit_sha.to_string(), path: path.to_string(), path_bytes: Vec::new(), line_number: None, resolved, }) .await .map_err(BabyError::from)? .into_inner(); Ok(response.comments) } pub async fn create_code_comment( &self, revision: &str, path: &str, line_number: u32, body: &str, user_id: i64, parent_id: &str, ) -> Result { let mut client = self.context().ks.comment(); let response = client .create_code_comment(pb::CreateCodeCommentRequest { repository: self.repository_ref_some(), revision: revision.to_string(), path: path.to_string(), line_number, body: body.to_string(), user_id, path_bytes: Vec::new(), parent_id: parent_id.to_string(), }) .await .map_err(BabyError::from)? .into_inner(); response .comment .ok_or_else(|| BabyError::RefNotFound("code comment".to_string())) } pub async fn update_code_comment( &self, id: &str, user_id: i64, body: &str, resolved: Option, ) -> Result { let mut client = self.context().ks.comment(); let response = client .update_code_comment(pb::UpdateCodeCommentRequest { repository: self.repository_ref_some(), id: id.to_string(), user_id, body: body.to_string(), resolved, }) .await .map_err(BabyError::from)? .into_inner(); response .comment .ok_or_else(|| BabyError::RefNotFound("code comment".to_string())) } pub async fn delete_code_comment(&self, id: &str, user_id: i64) -> Result<(), BabyError> { let mut client = self.context().ks.comment(); client .delete_code_comment(pb::DeleteCodeCommentRequest { repository: self.repository_ref_some(), id: id.to_string(), user_id, }) .await .map_err(BabyError::from)?; Ok(()) } pub async fn repo_size(&self) -> Result { let mut client = self.context().ks.repository(); let response = client .get_repository_size(pb::GetRepositorySizeRequest { repository: self.repository_ref_some(), }) .await .map_err(BabyError::from)? .into_inner(); Ok(response.size_bytes) } pub async fn merge_base(&self, _one: &str, _two: &str) -> Result { Err(unsupported("merge base")) } pub async fn merge_tree( &self, _ours: &str, _theirs: &str, _base: Option<&str>, _force: bool, ) -> Result { Err(unsupported("merge tree")) } pub async fn verify_tag_signature(&self, _name: &str) -> Result { Err(unsupported("tag signature verification")) } pub async fn list_submodules( &self, _commit: &str, ) -> Result, BabyError> { Err(unsupported("submodules")) } pub async fn prune(&self) -> Result<(), BabyError> { Err(unsupported("prune")) } } #[derive(Debug, Clone)] pub struct ArchiveArtifact { pub format: i32, pub size: u64, pub storage_key: String, pub download_url: String, } fn tag_from_view(tag: pb::TagView) -> TagInfo { let target = oid(&tag.target_sha) .unwrap_or_else(|_| oid_opt("0000000000000000000000000000000000000000").expect("zero")); TagInfo { name: tag.name, sha: target.clone(), target_sha: target, tag_type: if tag.annotated { TagType::Annotated } else { TagType::Lightweight }, tagger: if tag.tagger_name.is_empty() && tag.tagger_email.is_empty() { None } else { Some(Signature { name: tag.tagger_name, email: tag.tagger_email, time: tag .created_at .and_then(|at| time::OffsetDateTime::from_unix_timestamp(at.seconds).ok()) .unwrap_or(time::OffsetDateTime::UNIX_EPOCH), }) }, message: if tag.message.is_empty() { None } else { Some(tag.message) }, created_at: at(tag.created_at), } } fn changed_path_diff(file: pb::DiffFileView) -> ChangedPathDiff { let mut patch = String::new(); for hunk in &file.hunks { patch.push_str(&hunk.header); patch.push('\n'); for line in &hunk.lines { let prefix = match pb::DiffLineKind::try_from(line.kind).unwrap_or(pb::DiffLineKind::Context) { pb::DiffLineKind::Addition => "+", pb::DiffLineKind::Deletion => "-", pb::DiffLineKind::NoNewline => "\\", _ => " ", }; patch.push_str(prefix); patch.push_str(&line.content); patch.push('\n'); } } let status = match pb::DiffFileStatus::try_from(file.status).unwrap_or(pb::DiffFileStatus::Modified) { pb::DiffFileStatus::Added => DiffStatus::Added, pb::DiffFileStatus::Deleted => DiffStatus::Deleted, pb::DiffFileStatus::Renamed => DiffStatus::Renamed, pb::DiffFileStatus::Copied => DiffStatus::Copied, pb::DiffFileStatus::TypeChanged => DiffStatus::TypeChanged, _ => DiffStatus::Modified, }; let old_path = if file.old_path.is_empty() { None } else { Some(PathBuf::from(file.old_path)) }; let new_path = if file.new_path.is_empty() { old_path.clone().unwrap_or_default() } else { PathBuf::from(file.new_path) }; ChangedPathDiff { path: ChangedPath { old_path, new_path, old_mode: 0, new_mode: 0, old_oid: oid_opt(&file.old_oid), new_oid: oid_opt(&file.new_oid), status, }, lines_added: file.additions as u64, lines_removed: file.deletions as u64, binary: file.binary, patch: if patch.is_empty() || file.binary { None } else { Some(patch.into_bytes()) }, } } fn archive_format(format: ArchiveFormat) -> pb::ArchiveFormat { match format { ArchiveFormat::Tar => pb::ArchiveFormat::Tar, ArchiveFormat::TarGz => pb::ArchiveFormat::TarGz, ArchiveFormat::TarBz2 => pb::ArchiveFormat::TarBz2, ArchiveFormat::Zip => pb::ArchiveFormat::Zip, } } impl From for BabyError { fn from(status: tonic::Status) -> Self { match status.code() { tonic::Code::NotFound => BabyError::RevisionNotFound(status.message().to_string()), tonic::Code::AlreadyExists => BabyError::Custom(status.message().to_string()), tonic::Code::Aborted | tonic::Code::FailedPrecondition => { BabyError::RefUpdateRejected(status.message().to_string()) } tonic::Code::InvalidArgument => { BabyError::RevisionInvalid(status.message().to_string()) } tonic::Code::Unauthenticated => BabyError::Custom("unauthenticated".to_string()), tonic::Code::PermissionDenied => BabyError::Custom("permission denied".to_string()), tonic::Code::Unimplemented => BabyError::Custom(status.message().to_string()), _ => BabyError::Custom(format!("{}: {}", status.code(), status.message())), } } }