feat(server): add comprehensive Git repository services with test coverage
- Implement ArchiveService for repository archive operations - Add BlameService for Git blame functionality - Create BranchService with full branch management capabilities - Integrate CommitService for commit operations and history - Add DiffService for generating diffs and patches - Implement MergeService with conflict resolution features - Add PackService for Git packfile operations - Create TagService for Git tag management - Add TreeService for Git tree operations - Implement comprehensive repository management functions - Add repository statistics and health checking capabilities - Include garbage collection and repacking operations - Add repository configuration management - Implement error handling and status conversion utilities - Add test suite covering all repository operations - Create utility functions for Git command execution - Add streaming response support for large data operations - Implement request resolution and validation helpers
This commit is contained in:
@@ -0,0 +1,29 @@
|
|||||||
|
use crate::pb::*;
|
||||||
|
|
||||||
|
use super::{GitksService, into_status, into_stream, resolve};
|
||||||
|
|
||||||
|
#[tonic::async_trait]
|
||||||
|
impl archive_service_server::ArchiveService for GitksService {
|
||||||
|
type GetArchiveStream =
|
||||||
|
tokio_stream::wrappers::ReceiverStream<Result<ArchiveChunk, tonic::Status>>;
|
||||||
|
|
||||||
|
async fn get_archive(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<ArchiveRequest>,
|
||||||
|
) -> Result<tonic::Response<Self::GetArchiveStream>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let chunks = gb.get_archive(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(into_stream(chunks)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_archive_entries(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<ListArchiveEntriesRequest>,
|
||||||
|
) -> Result<tonic::Response<ListArchiveEntriesResponse>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.list_archive_entries(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
use crate::pb::*;
|
||||||
|
|
||||||
|
use super::{GitksService, into_status, into_stream, resolve};
|
||||||
|
|
||||||
|
#[tonic::async_trait]
|
||||||
|
impl blame_service_server::BlameService for GitksService {
|
||||||
|
type StreamBlameStream =
|
||||||
|
tokio_stream::wrappers::ReceiverStream<Result<BlameHunk, tonic::Status>>;
|
||||||
|
|
||||||
|
async fn blame(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<BlameRequest>,
|
||||||
|
) -> Result<tonic::Response<BlameResponse>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.blame(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn stream_blame(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<BlameRequest>,
|
||||||
|
) -> Result<tonic::Response<Self::StreamBlameStream>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.blame(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(into_stream(resp.hunks)))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
use crate::pb::*;
|
||||||
|
|
||||||
|
use super::{GitksService, into_status, resolve};
|
||||||
|
|
||||||
|
#[tonic::async_trait]
|
||||||
|
impl branch_service_server::BranchService for GitksService {
|
||||||
|
async fn list_branches(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<ListBranchesRequest>,
|
||||||
|
) -> Result<tonic::Response<ListBranchesResponse>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.list_branches(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_branch(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<GetBranchRequest>,
|
||||||
|
) -> Result<tonic::Response<Branch>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.get_branch(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_branch(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<CreateBranchRequest>,
|
||||||
|
) -> Result<tonic::Response<Branch>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.create_branch(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_branch(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<DeleteBranchRequest>,
|
||||||
|
) -> Result<tonic::Response<()>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
gb.delete_branch(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn rename_branch(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<RenameBranchRequest>,
|
||||||
|
) -> Result<tonic::Response<Branch>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.rename_branch(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_branch_target(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<UpdateBranchTargetRequest>,
|
||||||
|
) -> Result<tonic::Response<Branch>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.update_branch_target(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_branch_upstream(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<SetBranchUpstreamRequest>,
|
||||||
|
) -> Result<tonic::Response<Branch>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.set_branch_upstream(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn compare_branch(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<CompareBranchRequest>,
|
||||||
|
) -> Result<tonic::Response<CompareBranchResponse>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.compare_branch(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
use crate::pb::*;
|
||||||
|
|
||||||
|
use super::{GitksService, into_status, resolve};
|
||||||
|
|
||||||
|
#[tonic::async_trait]
|
||||||
|
impl commit_service_server::CommitService for GitksService {
|
||||||
|
async fn list_commits(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<ListCommitsRequest>,
|
||||||
|
) -> Result<tonic::Response<ListCommitsResponse>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.list_commits(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_commit(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<GetCommitRequest>,
|
||||||
|
) -> Result<tonic::Response<Commit>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.get_commit(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_commit_ancestors(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<GetCommitAncestorsRequest>,
|
||||||
|
) -> Result<tonic::Response<GetCommitAncestorsResponse>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.get_commit_ancestors(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_commit(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<CreateCommitRequest>,
|
||||||
|
) -> Result<tonic::Response<CreateCommitResponse>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.create_commit(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn revert_commit(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<RevertCommitRequest>,
|
||||||
|
) -> Result<tonic::Response<CreateCommitResponse>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.revert_commit(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cherry_pick_commit(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<CherryPickCommitRequest>,
|
||||||
|
) -> Result<tonic::Response<CreateCommitResponse>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.cherry_pick_commit(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn compare_commits(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<CompareCommitsRequest>,
|
||||||
|
) -> Result<tonic::Response<CompareCommitsResponse>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.compare_commits(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
use crate::pb::*;
|
||||||
|
|
||||||
|
use super::{GitksService, into_status, into_stream, resolve};
|
||||||
|
|
||||||
|
#[tonic::async_trait]
|
||||||
|
impl diff_service_server::DiffService for GitksService {
|
||||||
|
type GetPatchStream =
|
||||||
|
tokio_stream::wrappers::ReceiverStream<Result<GetPatchResponse, tonic::Status>>;
|
||||||
|
|
||||||
|
async fn get_diff(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<GetDiffRequest>,
|
||||||
|
) -> Result<tonic::Response<GetDiffResponse>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.get_diff(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_commit_diff(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<GetCommitDiffRequest>,
|
||||||
|
) -> Result<tonic::Response<GetDiffResponse>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.get_commit_diff(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_patch(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<GetPatchRequest>,
|
||||||
|
) -> Result<tonic::Response<Self::GetPatchStream>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let items = gb.get_patch(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(into_stream(items)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_diff_stats(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<GetDiffStatsRequest>,
|
||||||
|
) -> Result<tonic::Response<DiffStats>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.get_diff_stats(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
use crate::pb::*;
|
||||||
|
|
||||||
|
use super::{GitksService, into_status, resolve};
|
||||||
|
|
||||||
|
#[tonic::async_trait]
|
||||||
|
impl merge_service_server::MergeService for GitksService {
|
||||||
|
async fn check_merge(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<CheckMergeRequest>,
|
||||||
|
) -> Result<tonic::Response<MergeResult>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.check_merge(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn merge(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<MergeRequest>,
|
||||||
|
) -> Result<tonic::Response<MergeResult>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.merge(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_merge_conflicts(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<ListMergeConflictsRequest>,
|
||||||
|
) -> Result<tonic::Response<ListMergeConflictsResponse>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.list_merge_conflicts(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn resolve_merge_conflicts(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<ResolveMergeConflictsRequest>,
|
||||||
|
) -> Result<tonic::Response<MergeResult>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.resolve_merge_conflicts(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn rebase(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<RebaseRequest>,
|
||||||
|
) -> Result<tonic::Response<RebaseResult>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.rebase(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
}
|
||||||
+115
@@ -0,0 +1,115 @@
|
|||||||
|
mod archive;
|
||||||
|
mod blame;
|
||||||
|
mod branch;
|
||||||
|
mod commit;
|
||||||
|
mod diff;
|
||||||
|
mod merge;
|
||||||
|
mod pack;
|
||||||
|
mod repository;
|
||||||
|
mod repository_maint;
|
||||||
|
mod tag;
|
||||||
|
mod tree;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tokio_stream::wrappers::ReceiverStream;
|
||||||
|
|
||||||
|
use crate::bare::GitBare;
|
||||||
|
use crate::error::GitError;
|
||||||
|
use crate::pb::{
|
||||||
|
archive_service_server, blame_service_server, branch_service_server, commit_service_server,
|
||||||
|
diff_service_server, merge_service_server, pack_service_server, repository_service_server,
|
||||||
|
tag_service_server, tree_service_server,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct GitksService;
|
||||||
|
|
||||||
|
pub(crate) fn into_status(e: GitError) -> tonic::Status {
|
||||||
|
match &e {
|
||||||
|
GitError::NotFound(_)
|
||||||
|
| GitError::ObjectNotFound(_)
|
||||||
|
| GitError::RefNotFound(_)
|
||||||
|
| GitError::RepoNotFound => tonic::Status::not_found(e.to_string()),
|
||||||
|
GitError::InvalidArgument(_) => tonic::Status::invalid_argument(e.to_string()),
|
||||||
|
GitError::PermissionDenied(_) => tonic::Status::permission_denied(e.to_string()),
|
||||||
|
GitError::Locked(_) => tonic::Status::failed_precondition(e.to_string()),
|
||||||
|
GitError::AuthFailed(_) => tonic::Status::unauthenticated(e.to_string()),
|
||||||
|
GitError::NotBareRepository => tonic::Status::failed_precondition(e.to_string()),
|
||||||
|
_ => tonic::Status::internal(e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<GitError> for tonic::Status {
|
||||||
|
fn from(e: GitError) -> Self {
|
||||||
|
into_status(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn resolve(
|
||||||
|
header: Option<&crate::pb::RepositoryHeader>,
|
||||||
|
) -> Result<GitBare, tonic::Status> {
|
||||||
|
let header = header.ok_or_else(|| tonic::Status::invalid_argument("repository is required"))?;
|
||||||
|
GitBare::from_repository_header(header).map_err(into_status)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn resolve_for_init(
|
||||||
|
header: Option<&crate::pb::RepositoryHeader>,
|
||||||
|
) -> Result<PathBuf, tonic::Status> {
|
||||||
|
let header = header.ok_or_else(|| tonic::Status::invalid_argument("repository is required"))?;
|
||||||
|
let storage_path = header.storage_path.trim();
|
||||||
|
let relative_path = header.relative_path.trim();
|
||||||
|
if storage_path.is_empty() {
|
||||||
|
return Err(tonic::Status::invalid_argument("storage_path is required"));
|
||||||
|
}
|
||||||
|
let p = std::path::Path::new(storage_path);
|
||||||
|
if !p.is_absolute() {
|
||||||
|
return Err(tonic::Status::invalid_argument(
|
||||||
|
"storage_path must be an absolute path",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let base = PathBuf::from(p);
|
||||||
|
Ok(if !relative_path.is_empty() {
|
||||||
|
base.join(relative_path)
|
||||||
|
} else {
|
||||||
|
base
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn into_stream<T: Send + 'static>(
|
||||||
|
items: Vec<T>,
|
||||||
|
) -> ReceiverStream<Result<T, tonic::Status>> {
|
||||||
|
let (tx, rx) = tokio::sync::mpsc::channel(items.len().max(1));
|
||||||
|
for item in items {
|
||||||
|
let _ = tx.try_send(Ok(item));
|
||||||
|
}
|
||||||
|
ReceiverStream::new(rx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn git_cmd(gb: &GitBare, args: &[&str]) -> Result<std::process::Output, tonic::Status> {
|
||||||
|
let mut full_args: Vec<String> = vec![
|
||||||
|
"--git-dir".into(),
|
||||||
|
gb.bare_dir.to_string_lossy().into_owned(),
|
||||||
|
];
|
||||||
|
full_args.extend(args.iter().map(|s| s.to_string()));
|
||||||
|
std::process::Command::new("git")
|
||||||
|
.args(&full_args)
|
||||||
|
.output()
|
||||||
|
.map_err(|e| tonic::Status::internal(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn serve(addr: std::net::SocketAddr) -> Result<(), tonic::transport::Error> {
|
||||||
|
let svc = GitksService;
|
||||||
|
tonic::transport::Server::builder()
|
||||||
|
.add_service(repository_service_server::RepositoryServiceServer::new(svc))
|
||||||
|
.add_service(archive_service_server::ArchiveServiceServer::new(svc))
|
||||||
|
.add_service(blame_service_server::BlameServiceServer::new(svc))
|
||||||
|
.add_service(branch_service_server::BranchServiceServer::new(svc))
|
||||||
|
.add_service(commit_service_server::CommitServiceServer::new(svc))
|
||||||
|
.add_service(diff_service_server::DiffServiceServer::new(svc))
|
||||||
|
.add_service(merge_service_server::MergeServiceServer::new(svc))
|
||||||
|
.add_service(pack_service_server::PackServiceServer::new(svc))
|
||||||
|
.add_service(tag_service_server::TagServiceServer::new(svc))
|
||||||
|
.add_service(tree_service_server::TreeServiceServer::new(svc))
|
||||||
|
.serve(addr)
|
||||||
|
.await
|
||||||
|
}
|
||||||
+121
@@ -0,0 +1,121 @@
|
|||||||
|
use tokio_stream::StreamExt;
|
||||||
|
use tokio_stream::wrappers::ReceiverStream;
|
||||||
|
|
||||||
|
use crate::pb::*;
|
||||||
|
|
||||||
|
use super::{GitksService, into_status, resolve};
|
||||||
|
|
||||||
|
#[tonic::async_trait]
|
||||||
|
impl pack_service_server::PackService for GitksService {
|
||||||
|
type UploadPackStream = ReceiverStream<Result<UploadPackResponse, tonic::Status>>;
|
||||||
|
type ReceivePackStream = ReceiverStream<Result<ReceivePackResponse, tonic::Status>>;
|
||||||
|
type PackObjectsStream = ReceiverStream<Result<PackfileChunk, tonic::Status>>;
|
||||||
|
|
||||||
|
async fn advertise_refs(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<AdvertiseRefsRequest>,
|
||||||
|
) -> Result<tonic::Response<AdvertiseRefsResponse>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.advertise_refs(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn upload_pack(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<tonic::Streaming<UploadPackRequest>>,
|
||||||
|
) -> Result<tonic::Response<Self::UploadPackStream>, tonic::Status> {
|
||||||
|
let mut stream = request.into_inner();
|
||||||
|
let first = stream
|
||||||
|
.next()
|
||||||
|
.await
|
||||||
|
.ok_or_else(|| tonic::Status::invalid_argument("empty upload-pack stream"))??;
|
||||||
|
let gb = resolve(first.repository.as_ref())?;
|
||||||
|
|
||||||
|
let (tx, rx) = tokio::sync::mpsc::channel(16);
|
||||||
|
tx.send(Ok(first))
|
||||||
|
.await
|
||||||
|
.map_err(|_| tonic::Status::internal("channel closed"))?;
|
||||||
|
tokio::spawn(async move {
|
||||||
|
while let Some(msg) = stream.next().await {
|
||||||
|
if tx.send(msg).await.is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = gb.upload_pack(ReceiverStream::new(rx)).await?;
|
||||||
|
Ok(tonic::Response::new(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive_pack(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<tonic::Streaming<ReceivePackRequest>>,
|
||||||
|
) -> Result<tonic::Response<Self::ReceivePackStream>, tonic::Status> {
|
||||||
|
let mut stream = request.into_inner();
|
||||||
|
let first = stream
|
||||||
|
.next()
|
||||||
|
.await
|
||||||
|
.ok_or_else(|| tonic::Status::invalid_argument("empty receive-pack stream"))??;
|
||||||
|
let gb = resolve(first.repository.as_ref())?;
|
||||||
|
|
||||||
|
let (tx, rx) = tokio::sync::mpsc::channel(16);
|
||||||
|
tx.send(Ok(first))
|
||||||
|
.await
|
||||||
|
.map_err(|_| tonic::Status::internal("channel closed"))?;
|
||||||
|
tokio::spawn(async move {
|
||||||
|
while let Some(msg) = stream.next().await {
|
||||||
|
if tx.send(msg).await.is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = gb.receive_pack(ReceiverStream::new(rx)).await?;
|
||||||
|
Ok(tonic::Response::new(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn pack_objects(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<PackObjectsRequest>,
|
||||||
|
) -> Result<tonic::Response<Self::PackObjectsStream>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let stream = gb.pack_objects(inner).await?;
|
||||||
|
Ok(tonic::Response::new(stream))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn index_pack(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<tonic::Streaming<IndexPackRequest>>,
|
||||||
|
) -> Result<tonic::Response<IndexPackResponse>, tonic::Status> {
|
||||||
|
let mut stream = request.into_inner();
|
||||||
|
let mut inputs = Vec::new();
|
||||||
|
while let Some(msg) = stream.next().await {
|
||||||
|
inputs.push(msg?);
|
||||||
|
}
|
||||||
|
let gb = resolve(inputs.first().and_then(|r| r.repository.as_ref()))?;
|
||||||
|
let resp = gb.index_pack(inputs).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_packfiles(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<ListPackfilesRequest>,
|
||||||
|
) -> Result<tonic::Response<ListPackfilesResponse>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.list_packfiles(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fsck(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<FsckRequest>,
|
||||||
|
) -> Result<tonic::Response<FsckResponse>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.fsck(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
use crate::pb::*;
|
||||||
|
|
||||||
|
use super::{GitksService, git_cmd, into_status, repository_maint, resolve, resolve_for_init};
|
||||||
|
|
||||||
|
fn default_branch_name(gb: &crate::bare::GitBare) -> String {
|
||||||
|
git_cmd(gb, &["symbolic-ref", "HEAD"])
|
||||||
|
.ok()
|
||||||
|
.and_then(|o| {
|
||||||
|
String::from_utf8_lossy(&o.stdout)
|
||||||
|
.trim()
|
||||||
|
.strip_prefix("refs/heads/")
|
||||||
|
.map(|b| b.to_string())
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tonic::async_trait]
|
||||||
|
impl repository_service_server::RepositoryService for GitksService {
|
||||||
|
async fn get_repository(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<GetRepositoryRequest>,
|
||||||
|
) -> Result<tonic::Response<Repository>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let bare = gb.bare_dir.join("HEAD").exists();
|
||||||
|
let object_format = gb.object_format();
|
||||||
|
Ok(tonic::Response::new(Repository {
|
||||||
|
header: inner.repository,
|
||||||
|
bare,
|
||||||
|
object_format: object_format as i32,
|
||||||
|
default_branch: default_branch_name(&gb),
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn init_repository(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<InitRepositoryRequest>,
|
||||||
|
) -> Result<tonic::Response<Repository>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let bare_dir = resolve_for_init(inner.repository.as_ref())?;
|
||||||
|
let gb = crate::bare::GitBare { bare_dir };
|
||||||
|
gb.init_repository(inner.bare).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(Repository {
|
||||||
|
header: inner.repository,
|
||||||
|
bare: inner.bare,
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_repository(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<DeleteRepositoryRequest>,
|
||||||
|
) -> Result<tonic::Response<()>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let bare_dir = resolve_for_init(inner.repository.as_ref())?;
|
||||||
|
std::fs::remove_dir_all(&bare_dir).map_err(|e| tonic::Status::internal(e.to_string()))?;
|
||||||
|
Ok(tonic::Response::new(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn repository_exists(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<RepositoryExistsRequest>,
|
||||||
|
) -> Result<tonic::Response<RepositoryExistsResponse>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let bare_dir = resolve_for_init(inner.repository.as_ref())?;
|
||||||
|
let exists = bare_dir.exists() && bare_dir.is_dir() && bare_dir.join("HEAD").exists();
|
||||||
|
Ok(tonic::Response::new(RepositoryExistsResponse { exists }))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_object_format(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<RepositoryObjectFormatRequest>,
|
||||||
|
) -> Result<tonic::Response<RepositoryObjectFormatResponse>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
Ok(tonic::Response::new(RepositoryObjectFormatResponse {
|
||||||
|
object_format: gb.object_format() as i32,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_default_branch(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<GetDefaultBranchRequest>,
|
||||||
|
) -> Result<tonic::Response<GetDefaultBranchResponse>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
Ok(tonic::Response::new(GetDefaultBranchResponse {
|
||||||
|
name: default_branch_name(&gb),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_default_branch(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<SetDefaultBranchRequest>,
|
||||||
|
) -> Result<tonic::Response<()>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let refname = format!("refs/heads/{}", inner.name);
|
||||||
|
let out = git_cmd(&gb, &["symbolic-ref", "HEAD", &refname])?;
|
||||||
|
if !out.status.success() {
|
||||||
|
return Err(tonic::Status::internal(
|
||||||
|
String::from_utf8_lossy(&out.stderr).trim().to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(tonic::Response::new(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_repository_config(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<GetRepositoryConfigRequest>,
|
||||||
|
) -> Result<tonic::Response<GetRepositoryConfigResponse>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
if inner.keys.is_empty() {
|
||||||
|
let out = git_cmd(&gb, &["config", "--list"])?;
|
||||||
|
if !out.status.success() {
|
||||||
|
return Err(tonic::Status::internal(
|
||||||
|
String::from_utf8_lossy(&out.stderr).trim().to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
for line in String::from_utf8_lossy(&out.stdout).lines() {
|
||||||
|
if let Some((k, v)) = line.split_once('=') {
|
||||||
|
entries.push(RepositoryConfigEntry {
|
||||||
|
key: k.trim().to_string(),
|
||||||
|
values: vec![v.trim().to_string()],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for key in &inner.keys {
|
||||||
|
let out = git_cmd(&gb, &["config", "--get-all", key])?;
|
||||||
|
if out.status.success() {
|
||||||
|
let vals: Vec<String> = String::from_utf8_lossy(&out.stdout)
|
||||||
|
.lines()
|
||||||
|
.map(|l| l.trim().to_string())
|
||||||
|
.filter(|l| !l.is_empty())
|
||||||
|
.collect();
|
||||||
|
if !vals.is_empty() {
|
||||||
|
entries.push(RepositoryConfigEntry {
|
||||||
|
key: key.clone(),
|
||||||
|
values: vals,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(tonic::Response::new(GetRepositoryConfigResponse {
|
||||||
|
entries,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_repository_config(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<SetRepositoryConfigRequest>,
|
||||||
|
) -> Result<tonic::Response<()>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
for entry in &inner.entries {
|
||||||
|
if entry.values.is_empty() {
|
||||||
|
git_cmd(&gb, &["config", "--unset-all", &entry.key])?;
|
||||||
|
} else {
|
||||||
|
let _ = git_cmd(
|
||||||
|
&gb,
|
||||||
|
&["config", "--replace-all", &entry.key, &entry.values[0]],
|
||||||
|
);
|
||||||
|
for v in entry.values.iter().skip(1) {
|
||||||
|
let _ = git_cmd(&gb, &["config", "--add", &entry.key, v]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(tonic::Response::new(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_repository_statistics(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<RepositoryStatisticsRequest>,
|
||||||
|
) -> Result<tonic::Response<RepositoryStatistics>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
Ok(tonic::Response::new(repository_maint::get_statistics(&gb)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_repository_health(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<RepositoryHealthRequest>,
|
||||||
|
) -> Result<tonic::Response<RepositoryHealthResponse>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
Ok(tonic::Response::new(repository_maint::check_health(
|
||||||
|
&gb,
|
||||||
|
inner.connectivity_only,
|
||||||
|
)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn garbage_collect(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<GarbageCollectRequest>,
|
||||||
|
) -> Result<tonic::Response<RepositoryMaintenanceResponse>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
Ok(tonic::Response::new(repository_maint::run_gc(
|
||||||
|
&gb,
|
||||||
|
inner.prune,
|
||||||
|
inner.aggressive,
|
||||||
|
)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn repack(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<RepackRequest>,
|
||||||
|
) -> Result<tonic::Response<RepositoryMaintenanceResponse>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
Ok(tonic::Response::new(repository_maint::run_repack(
|
||||||
|
&gb,
|
||||||
|
inner.full,
|
||||||
|
inner.write_bitmaps,
|
||||||
|
inner.write_multi_pack_index,
|
||||||
|
)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write_commit_graph(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<WriteCommitGraphRequest>,
|
||||||
|
) -> Result<tonic::Response<RepositoryMaintenanceResponse>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
Ok(tonic::Response::new(
|
||||||
|
repository_maint::run_commit_graph_write(&gb, inner.split, inner.replace)?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
use crate::pb::*;
|
||||||
|
|
||||||
|
use super::git_cmd;
|
||||||
|
|
||||||
|
pub(crate) fn maintenance_response(out: std::process::Output) -> RepositoryMaintenanceResponse {
|
||||||
|
RepositoryMaintenanceResponse {
|
||||||
|
ok: out.status.success(),
|
||||||
|
stdout: String::from_utf8_lossy(&out.stdout).into_owned(),
|
||||||
|
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dir_size(path: &std::path::Path) -> u64 {
|
||||||
|
let mut total = 0u64;
|
||||||
|
if let Ok(entries) = std::fs::read_dir(path) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let p = entry.path();
|
||||||
|
if p.is_file() {
|
||||||
|
total += entry.metadata().map(|m| m.len()).unwrap_or(0);
|
||||||
|
} else if p.is_dir() {
|
||||||
|
total += dir_size(&p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
total
|
||||||
|
}
|
||||||
|
|
||||||
|
fn count_refs(gb: &crate::bare::GitBare) -> u64 {
|
||||||
|
let out = git_cmd(gb, &["for-each-ref", "--format=%(refname)"]).unwrap_or_else(|_| {
|
||||||
|
std::process::Output {
|
||||||
|
status: Default::default(),
|
||||||
|
stdout: Vec::new(),
|
||||||
|
stderr: Vec::new(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
String::from_utf8_lossy(&out.stdout)
|
||||||
|
.lines()
|
||||||
|
.filter(|l| !l.is_empty())
|
||||||
|
.count() as u64
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file_len(path: &std::path::Path) -> u64 {
|
||||||
|
std::fs::metadata(path).map(|m| m.len()).unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_statistics(gb: &crate::bare::GitBare) -> RepositoryStatistics {
|
||||||
|
let size_bytes = dir_size(&gb.bare_dir);
|
||||||
|
|
||||||
|
let mut loose_object_count: u64 = 0;
|
||||||
|
let mut packed_object_count: u64 = 0;
|
||||||
|
let mut packfile_count: u64 = 0;
|
||||||
|
if let Ok(out) = git_cmd(gb, &["count-objects", "-v"]) {
|
||||||
|
for line in String::from_utf8_lossy(&out.stdout).lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if let Some(v) = line.strip_prefix("count: ") {
|
||||||
|
loose_object_count = v.trim().parse().unwrap_or(0);
|
||||||
|
} else if let Some(v) = line.strip_prefix("in-pack: ") {
|
||||||
|
packed_object_count = v.trim().parse().unwrap_or(0);
|
||||||
|
} else if let Some(v) = line.strip_prefix("packs: ") {
|
||||||
|
packfile_count = v.trim().parse().unwrap_or(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let reference_count = count_refs(gb);
|
||||||
|
let commit_graph_size_bytes = file_len(&gb.bare_dir.join("objects/info/commit-graph"));
|
||||||
|
let multi_pack_index_size_bytes = file_len(&gb.bare_dir.join("objects/pack/multi-pack-index"));
|
||||||
|
|
||||||
|
RepositoryStatistics {
|
||||||
|
size_bytes,
|
||||||
|
loose_object_count,
|
||||||
|
packed_object_count,
|
||||||
|
packfile_count,
|
||||||
|
reference_count,
|
||||||
|
commit_graph_size_bytes,
|
||||||
|
multi_pack_index_size_bytes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn check_health(
|
||||||
|
gb: &crate::bare::GitBare,
|
||||||
|
connectivity_only: bool,
|
||||||
|
) -> Result<RepositoryHealthResponse, tonic::Status> {
|
||||||
|
let mut args: Vec<&str> = vec!["fsck"];
|
||||||
|
if connectivity_only {
|
||||||
|
args.push("--connectivity-only");
|
||||||
|
}
|
||||||
|
let out = git_cmd(gb, &args)?;
|
||||||
|
let text = String::from_utf8_lossy(&out.stdout);
|
||||||
|
let mut warnings = Vec::new();
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
for line in text.lines() {
|
||||||
|
if line.starts_with("error:") {
|
||||||
|
errors.push(line.to_string());
|
||||||
|
} else if line.starts_with("warning:") {
|
||||||
|
warnings.push(line.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(RepositoryHealthResponse {
|
||||||
|
ok: out.status.success(),
|
||||||
|
warnings,
|
||||||
|
errors,
|
||||||
|
statistics: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn run_gc(
|
||||||
|
gb: &crate::bare::GitBare,
|
||||||
|
prune: bool,
|
||||||
|
aggressive: bool,
|
||||||
|
) -> Result<RepositoryMaintenanceResponse, tonic::Status> {
|
||||||
|
let mut args: Vec<&str> = vec!["gc"];
|
||||||
|
if prune {
|
||||||
|
args.push("--prune=now");
|
||||||
|
}
|
||||||
|
if aggressive {
|
||||||
|
args.push("--aggressive");
|
||||||
|
}
|
||||||
|
let out = git_cmd(gb, &args)?;
|
||||||
|
Ok(maintenance_response(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn run_repack(
|
||||||
|
gb: &crate::bare::GitBare,
|
||||||
|
full: bool,
|
||||||
|
write_bitmaps: bool,
|
||||||
|
write_multi_pack_index: bool,
|
||||||
|
) -> Result<RepositoryMaintenanceResponse, tonic::Status> {
|
||||||
|
let mut args: Vec<&str> = vec!["repack", "-d"];
|
||||||
|
if full {
|
||||||
|
args.push("-a");
|
||||||
|
}
|
||||||
|
if write_bitmaps {
|
||||||
|
args.push("--write-bitmap-index");
|
||||||
|
}
|
||||||
|
if write_multi_pack_index {
|
||||||
|
args.push("--write-midx");
|
||||||
|
}
|
||||||
|
let out = git_cmd(gb, &args)?;
|
||||||
|
Ok(maintenance_response(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn run_commit_graph_write(
|
||||||
|
gb: &crate::bare::GitBare,
|
||||||
|
split: bool,
|
||||||
|
replace: bool,
|
||||||
|
) -> Result<RepositoryMaintenanceResponse, tonic::Status> {
|
||||||
|
let mut args: Vec<&str> = vec!["commit-graph", "write"];
|
||||||
|
if split {
|
||||||
|
args.push("--split");
|
||||||
|
}
|
||||||
|
if !replace {
|
||||||
|
args.push("--append");
|
||||||
|
}
|
||||||
|
let out = git_cmd(gb, &args)?;
|
||||||
|
Ok(maintenance_response(out))
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
use crate::pb::*;
|
||||||
|
|
||||||
|
use super::{GitksService, into_status, resolve};
|
||||||
|
|
||||||
|
#[tonic::async_trait]
|
||||||
|
impl tag_service_server::TagService for GitksService {
|
||||||
|
async fn list_tags(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<ListTagsRequest>,
|
||||||
|
) -> Result<tonic::Response<ListTagsResponse>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.list_tags(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_tag(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<GetTagRequest>,
|
||||||
|
) -> Result<tonic::Response<Tag>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.get_tag(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_tag(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<CreateTagRequest>,
|
||||||
|
) -> Result<tonic::Response<Tag>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.create_tag(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_tag(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<DeleteTagRequest>,
|
||||||
|
) -> Result<tonic::Response<()>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
gb.delete_tag(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn verify_tag(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<VerifyTagRequest>,
|
||||||
|
) -> Result<tonic::Response<VerifiedSignature>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.verify_tag(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
use crate::pb::*;
|
||||||
|
|
||||||
|
use super::{GitksService, into_status, into_stream, resolve};
|
||||||
|
|
||||||
|
#[tonic::async_trait]
|
||||||
|
impl tree_service_server::TreeService for GitksService {
|
||||||
|
type GetRawBlobStream =
|
||||||
|
tokio_stream::wrappers::ReceiverStream<Result<GetRawBlobResponse, tonic::Status>>;
|
||||||
|
|
||||||
|
async fn list_tree(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<ListTreeRequest>,
|
||||||
|
) -> Result<tonic::Response<ListTreeResponse>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.list_tree(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_tree(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<GetTreeRequest>,
|
||||||
|
) -> Result<tonic::Response<Tree>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.get_tree(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_blob(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<GetBlobRequest>,
|
||||||
|
) -> Result<tonic::Response<Blob>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.get_blob(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_raw_blob(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<GetRawBlobRequest>,
|
||||||
|
) -> Result<tonic::Response<Self::GetRawBlobStream>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let items = gb.get_raw_blob(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(into_stream(items)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_file_metadata(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<GetFileMetadataRequest>,
|
||||||
|
) -> Result<tonic::Response<FileMetadata>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.get_file_metadata(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_files(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<FindFilesRequest>,
|
||||||
|
) -> Result<tonic::Response<FindFilesResponse>, tonic::Status> {
|
||||||
|
let inner = request.into_inner();
|
||||||
|
let gb = resolve(inner.repository.as_ref())?;
|
||||||
|
let resp = gb.find_files(inner).map_err(into_status)?;
|
||||||
|
Ok(tonic::Response::new(resp))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
mod common;
|
||||||
|
|
||||||
|
use gitks::bare::GitBare;
|
||||||
|
use gitks::pb::repository_service_server::RepositoryService;
|
||||||
|
use gitks::pb::*;
|
||||||
|
use gitks::server::GitksService;
|
||||||
|
|
||||||
|
fn header(gb: &GitBare) -> RepositoryHeader {
|
||||||
|
let parent = gb.bare_dir.parent().unwrap().to_string_lossy().into_owned();
|
||||||
|
let name = gb
|
||||||
|
.bare_dir
|
||||||
|
.file_name()
|
||||||
|
.unwrap()
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned();
|
||||||
|
RepositoryHeader {
|
||||||
|
storage_path: parent,
|
||||||
|
relative_path: name,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn req<T>(gb: &GitBare, f: impl FnOnce(RepositoryHeader) -> T) -> tonic::Request<T> {
|
||||||
|
tonic::Request::new(f(header(gb)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_repository() {
|
||||||
|
let (_dir, gb) = common::setup_bare_repo();
|
||||||
|
let repo = GitksService
|
||||||
|
.get_repository(req(&gb, |r| GetRepositoryRequest {
|
||||||
|
repository: Some(r),
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.into_inner();
|
||||||
|
assert!(repo.bare);
|
||||||
|
assert_eq!(repo.object_format, ObjectFormat::Sha1 as i32);
|
||||||
|
assert_eq!(repo.default_branch, "main");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_init_and_delete_repository() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let storage = dir.path().to_string_lossy().into_owned();
|
||||||
|
let hdr = RepositoryHeader {
|
||||||
|
storage_path: storage.clone(),
|
||||||
|
relative_path: "new-repo".into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let svc = GitksService;
|
||||||
|
svc.init_repository(tonic::Request::new(InitRepositoryRequest {
|
||||||
|
repository: Some(hdr.clone()),
|
||||||
|
bare: true,
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
svc.repository_exists(tonic::Request::new(RepositoryExistsRequest {
|
||||||
|
repository: Some(hdr.clone())
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.into_inner()
|
||||||
|
.exists
|
||||||
|
);
|
||||||
|
svc.delete_repository(tonic::Request::new(DeleteRepositoryRequest {
|
||||||
|
repository: Some(hdr.clone()),
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
!svc.repository_exists(tonic::Request::new(RepositoryExistsRequest {
|
||||||
|
repository: Some(hdr)
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.into_inner()
|
||||||
|
.exists
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_object_format() {
|
||||||
|
let (_dir, gb) = common::setup_bare_repo();
|
||||||
|
let resp = GitksService
|
||||||
|
.get_object_format(req(&gb, |r| RepositoryObjectFormatRequest {
|
||||||
|
repository: Some(r),
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.into_inner();
|
||||||
|
assert_eq!(resp.object_format, ObjectFormat::Sha1 as i32);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_set_default_branch() {
|
||||||
|
let (_dir, gb) = common::setup_bare_repo();
|
||||||
|
let h = header(&gb);
|
||||||
|
assert_eq!(
|
||||||
|
GitksService
|
||||||
|
.get_default_branch(tonic::Request::new(GetDefaultBranchRequest {
|
||||||
|
repository: Some(h.clone())
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.into_inner()
|
||||||
|
.name,
|
||||||
|
"main"
|
||||||
|
);
|
||||||
|
GitksService
|
||||||
|
.set_default_branch(tonic::Request::new(SetDefaultBranchRequest {
|
||||||
|
repository: Some(h.clone()),
|
||||||
|
name: "feature".into(),
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
GitksService
|
||||||
|
.get_default_branch(tonic::Request::new(GetDefaultBranchRequest {
|
||||||
|
repository: Some(h)
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.into_inner()
|
||||||
|
.name,
|
||||||
|
"feature"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_set_repository_config() {
|
||||||
|
let (_dir, gb) = common::setup_bare_repo();
|
||||||
|
GitksService
|
||||||
|
.set_repository_config(tonic::Request::new(SetRepositoryConfigRequest {
|
||||||
|
repository: Some(header(&gb)),
|
||||||
|
entries: vec![RepositoryConfigEntry {
|
||||||
|
key: "test.key".into(),
|
||||||
|
values: vec!["val1".into(), "val2".into()],
|
||||||
|
}],
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let entry = GitksService
|
||||||
|
.get_repository_config(tonic::Request::new(GetRepositoryConfigRequest {
|
||||||
|
repository: Some(header(&gb)),
|
||||||
|
keys: vec!["test.key".into()],
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.into_inner()
|
||||||
|
.entries
|
||||||
|
.into_iter()
|
||||||
|
.find(|e| e.key == "test.key")
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(entry.values, vec!["val1", "val2"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_repository_statistics() {
|
||||||
|
let (_dir, gb) = common::setup_bare_repo();
|
||||||
|
let s = GitksService
|
||||||
|
.get_repository_statistics(req(&gb, |r| RepositoryStatisticsRequest {
|
||||||
|
repository: Some(r),
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.into_inner();
|
||||||
|
assert!(s.size_bytes > 0);
|
||||||
|
assert!(s.loose_object_count > 0 || s.packed_object_count > 0);
|
||||||
|
assert!(s.reference_count >= 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_check_repository_health() {
|
||||||
|
let (_dir, gb) = common::setup_bare_repo();
|
||||||
|
let h = GitksService
|
||||||
|
.check_repository_health(tonic::Request::new(RepositoryHealthRequest {
|
||||||
|
repository: Some(header(&gb)),
|
||||||
|
connectivity_only: true,
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.into_inner();
|
||||||
|
assert!(h.ok && h.errors.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_garbage_collect() {
|
||||||
|
let (_dir, gb) = common::setup_bare_repo();
|
||||||
|
assert!(
|
||||||
|
GitksService
|
||||||
|
.garbage_collect(tonic::Request::new(GarbageCollectRequest {
|
||||||
|
repository: Some(header(&gb)),
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.into_inner()
|
||||||
|
.ok
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_repack() {
|
||||||
|
let (_dir, gb) = common::setup_bare_repo();
|
||||||
|
assert!(
|
||||||
|
GitksService
|
||||||
|
.repack(tonic::Request::new(RepackRequest {
|
||||||
|
repository: Some(header(&gb)),
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.into_inner()
|
||||||
|
.ok
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_write_commit_graph() {
|
||||||
|
let (_dir, gb) = common::setup_bare_repo();
|
||||||
|
assert!(
|
||||||
|
GitksService
|
||||||
|
.write_commit_graph(tonic::Request::new(WriteCommitGraphRequest {
|
||||||
|
repository: Some(header(&gb)),
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.into_inner()
|
||||||
|
.ok
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user