feat(core): implement Git repository operations with gRPC services

- Add advertise_refs functionality for Git protocol communication
- Implement archive service with TAR/ZIP format support and streaming
- Create blame service for Git file annotation with line tracking
- Add branch management including create, delete, rename and compare operations
- Implement merge checking with conflict detection and fast-forward handling
- Add cherry-pick functionality for applying commits between branches
- Integrate gix library for Git repository operations and object handling
- Add comprehensive test suite covering all Git operations
- Implement proper error handling and repository validation
- Add pagination support for large result sets
- Create protobuf definitions for all Git operations and data structures
- Add build system for gRPC code generation and dependency management
This commit is contained in:
zhenyi
2026-06-04 13:05:38 +08:00
commit dcb0fb74c5
98 changed files with 20569 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
use crate::bare::GitBare;
use crate::error::{GitError, GitResult};
use crate::pb::{CreateTagRequest, GetTagRequest, Tag};
impl GitBare {
pub fn create_tag(&self, request: CreateTagRequest) -> GitResult<Tag> {
let target = match request.target.and_then(|s| s.selector) {
Some(crate::pb::object_selector::Selector::Oid(oid)) => oid.hex,
Some(crate::pb::object_selector::Selector::Revision(name)) => name.revision,
None => "HEAD".into(),
};
let mut args = vec![
"--git-dir".to_string(),
self.bare_dir.to_string_lossy().into_owned(),
"tag".to_string(),
];
if request.force {
args.push("--force".into());
}
if request.annotated {
args.push("--annotate".into());
if !request.message.is_empty() {
args.push("-m".into());
args.push(request.message.clone());
}
}
args.push(request.name.clone());
args.push(target);
let result = duct::cmd("git", &args)
.stdout_capture()
.stderr_capture()
.unchecked()
.run()?;
if !result.status.success() {
return Err(GitError::CommandFailed {
status_code: result.status.code(),
stderr: String::from_utf8_lossy(&result.stderr).into_owned(),
});
}
self.get_tag(GetTagRequest {
repository: request.repository,
name: request.name,
include_raw: false,
})
}
}
+29
View File
@@ -0,0 +1,29 @@
use crate::bare::GitBare;
use crate::error::{GitError, GitResult};
use crate::pb::DeleteTagRequest;
impl GitBare {
pub fn delete_tag(&self, request: DeleteTagRequest) -> GitResult<()> {
let result = duct::cmd(
"git",
[
"--git-dir",
self.bare_dir.to_string_lossy().as_ref(),
"tag",
"-d",
&request.name,
],
)
.stdout_capture()
.stderr_capture()
.unchecked()
.run()?;
if !result.status.success() {
return Err(GitError::CommandFailed {
status_code: result.status.code(),
stderr: String::from_utf8_lossy(&result.stderr).into_owned(),
});
}
Ok(())
}
}
+63
View File
@@ -0,0 +1,63 @@
use gix::bstr::ByteSlice;
use crate::bare::GitBare;
use crate::error::GitResult;
use crate::pb::{GetTagRequest, ObjectType, Tag};
impl GitBare {
pub fn get_tag(&self, request: GetTagRequest) -> GitResult<Tag> {
let repo = self.gix_repo()?;
let refname = format!("refs/tags/{}", request.name);
let mut r = repo.find_reference(refname.as_str())?;
let full_ref = r.name().as_bstr().to_string();
let raw_target_hex = r.target().try_id().map(|id| id.to_string());
let peeled_hex = r.peel_to_id()?.to_string();
let is_annotated = raw_target_hex
.as_ref()
.is_some_and(|raw| *raw != peeled_hex);
let mut tag = Tag {
name: request.name,
full_ref,
target_oid: Some(self.oid_to_pb(peeled_hex)),
target_type: ObjectType::Commit as i32,
tag_oid: None,
annotated: false,
tagger: None,
message: String::new(),
signature: None,
raw: Vec::new(),
};
if is_annotated
&& let Some(ref raw_hex) = raw_target_hex
&& let Ok(id) = gix::hash::ObjectId::from_hex(raw_hex.as_bytes())
{
tag.tag_oid = Some(self.oid_to_pb(raw_hex.clone()));
if let Ok(obj) = repo.find_object(id)
&& let Ok(tag_obj) = obj.try_into_tag()
{
tag.annotated = true;
if let Ok(Some(tagger)) = tag_obj.tagger() {
tag.tagger = Some(crate::pb::Signature {
identity: Some(crate::pb::Identity {
name: tagger.name.to_string(),
email: tagger.email.to_string(),
}),
when: None,
timezone_offset: 0,
});
}
if let Ok(decoded) = tag_obj.decode() {
tag.message = String::from_utf8_lossy(decoded.message.trim()).into_owned();
}
if request.include_raw {
tag.raw = tag_obj.data.clone();
}
}
}
Ok(tag)
}
}
+41
View File
@@ -0,0 +1,41 @@
use crate::bare::GitBare;
use crate::error::GitResult;
use crate::paginate;
use crate::pb::{ListTagsRequest, ListTagsResponse, Tag};
impl GitBare {
pub fn list_tags(&self, request: ListTagsRequest) -> GitResult<ListTagsResponse> {
let repo = self.gix_repo()?;
let mut tags = Vec::new();
for r in repo.references()?.tags()? {
let mut r = r.map_err(|e| crate::error::GitError::Gix(e.to_string()))?;
let name = r.name().shorten().to_string();
if !request.pattern.is_empty() && !name.contains(&request.pattern) {
continue;
}
let hex = r
.peel_to_id()
.ok()
.map(|id| id.to_string())
.unwrap_or_default();
tags.push(Tag {
name: name.clone(),
full_ref: r.name().to_string(),
target_oid: Some(self.oid_to_pb(hex)),
target_type: crate::pb::ObjectType::Commit as i32,
tag_oid: None,
annotated: false,
tagger: None,
message: String::new(),
signature: None,
raw: Vec::new(),
});
}
paginate::apply_sort(&mut tags, request.sort_direction);
let (tags, page_info) = paginate::paginate(&tags, request.pagination.as_ref());
Ok(ListTagsResponse {
tags,
page_info: Some(page_info),
})
}
}
+5
View File
@@ -0,0 +1,5 @@
pub mod create_tag;
pub mod delete_tag;
pub mod get_tag;
pub mod list_tags;
pub mod verify_tag;
+35
View File
@@ -0,0 +1,35 @@
use crate::bare::GitBare;
use crate::error::GitResult;
use crate::pb::{VerifiedSignature, VerifyTagRequest};
impl GitBare {
pub fn verify_tag(&self, request: VerifyTagRequest) -> GitResult<VerifiedSignature> {
let result = duct::cmd(
"git",
[
"--git-dir",
self.bare_dir.to_string_lossy().as_ref(),
"tag",
"-v",
&request.name,
],
)
.stdout_capture()
.stderr_capture()
.unchecked()
.run()?;
let verified = result.status.success();
Ok(VerifiedSignature {
verified,
reason: if verified {
crate::pb::verified_signature::Reason::Valid as i32
} else {
crate::pb::verified_signature::Reason::GpgverifyError as i32
},
signature: String::new(),
payload: String::new(),
key_fingerprint: String::new(),
signer: String::new(),
})
}
}