cc202d6d1f
- Add tracing spans with repo labels for archive and blame operations - Implement caching for archive list entries when using OID selectors - Implement caching for blame operations when using OID selectors - Add detailed
187 lines
6.5 KiB
Rust
187 lines
6.5 KiB
Rust
mod archive;
|
|
mod blame;
|
|
mod branch;
|
|
mod cache;
|
|
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)]
|
|
pub struct GitksService {
|
|
/// Root prefix path for all repositories
|
|
pub repo_prefix: PathBuf,
|
|
}
|
|
|
|
impl GitksService {
|
|
fn repo_label(&self, header: Option<&crate::pb::RepositoryHeader>) -> String {
|
|
header
|
|
.and_then(|h| {
|
|
if h.relative_path.is_empty() {
|
|
None
|
|
} else {
|
|
Some(h.relative_path.clone())
|
|
}
|
|
})
|
|
.unwrap_or_else(|| "unknown".into())
|
|
}
|
|
|
|
pub(crate) fn resolve(
|
|
&self,
|
|
header: Option<&crate::pb::RepositoryHeader>,
|
|
) -> Result<GitBare, tonic::Status> {
|
|
let header =
|
|
header.ok_or_else(|| tonic::Status::invalid_argument("repository is required"))?;
|
|
let header = self.prefixed_header(header);
|
|
let gb = GitBare::from_repository_header(&header).map_err(into_status)?;
|
|
tracing::debug!(
|
|
repo = %gb.bare_dir.display(),
|
|
"resolved repository"
|
|
);
|
|
Ok(gb)
|
|
}
|
|
|
|
pub(crate) fn resolve_for_init(
|
|
&self,
|
|
header: Option<&crate::pb::RepositoryHeader>,
|
|
) -> Result<PathBuf, tonic::Status> {
|
|
let header =
|
|
header.ok_or_else(|| tonic::Status::invalid_argument("repository is required"))?;
|
|
let relative_path = header.relative_path.trim();
|
|
if relative_path.is_empty() {
|
|
return Err(tonic::Status::invalid_argument("relative_path is required"));
|
|
}
|
|
let candidate = self.repo_prefix.join(relative_path);
|
|
// Path traversal check
|
|
let canonical = candidate
|
|
.canonicalize()
|
|
.unwrap_or_else(|_| candidate.clone());
|
|
let prefix_canon = self
|
|
.repo_prefix
|
|
.canonicalize()
|
|
.unwrap_or_else(|_| self.repo_prefix.clone());
|
|
if !canonical.starts_with(&prefix_canon) {
|
|
return Err(tonic::Status::invalid_argument(
|
|
"path traversal detected: relative_path escapes repo prefix",
|
|
));
|
|
}
|
|
Ok(canonical)
|
|
}
|
|
|
|
/// Inject repo_prefix as storage_path into the client-provided header
|
|
fn prefixed_header(&self, header: &crate::pb::RepositoryHeader) -> crate::pb::RepositoryHeader {
|
|
crate::pb::RepositoryHeader {
|
|
storage_path: self.repo_prefix.to_string_lossy().into_owned(),
|
|
relative_path: header.relative_path.clone(),
|
|
storage_name: header.storage_name.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
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 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()));
|
|
tracing::debug!(
|
|
repo = %gb.bare_dir.display(),
|
|
args = %full_args.iter().skip(2).cloned().collect::<Vec<_>>().join(" "),
|
|
"spawning git subprocess"
|
|
);
|
|
let result = std::process::Command::new("git")
|
|
.args(&full_args)
|
|
.output()
|
|
.map_err(|e| {
|
|
tracing::error!(
|
|
repo = %gb.bare_dir.display(),
|
|
error = %e,
|
|
"failed to spawn git subprocess"
|
|
);
|
|
tonic::Status::internal(e.to_string())
|
|
})?;
|
|
if !result.status.success() {
|
|
let stderr = String::from_utf8_lossy(&result.stderr);
|
|
tracing::warn!(
|
|
repo = %gb.bare_dir.display(),
|
|
status = ?result.status.code(),
|
|
stderr = %stderr.trim(),
|
|
"git subprocess exited with non-zero status"
|
|
);
|
|
}
|
|
Ok(result)
|
|
}
|
|
|
|
pub async fn serve(
|
|
addr: std::net::SocketAddr,
|
|
repo_prefix: PathBuf,
|
|
) -> Result<(), tonic::transport::Error> {
|
|
let span = tracing::info_span!("gitks.server", %addr);
|
|
let _enter = span.enter();
|
|
let svc = GitksService { repo_prefix };
|
|
tracing::info!("registering gRPC services");
|
|
let server = tonic::transport::Server::builder()
|
|
.add_service(repository_service_server::RepositoryServiceServer::new(
|
|
svc.clone(),
|
|
))
|
|
.add_service(archive_service_server::ArchiveServiceServer::new(
|
|
svc.clone(),
|
|
))
|
|
.add_service(blame_service_server::BlameServiceServer::new(svc.clone()))
|
|
.add_service(branch_service_server::BranchServiceServer::new(svc.clone()))
|
|
.add_service(commit_service_server::CommitServiceServer::new(svc.clone()))
|
|
.add_service(diff_service_server::DiffServiceServer::new(svc.clone()))
|
|
.add_service(merge_service_server::MergeServiceServer::new(svc.clone()))
|
|
.add_service(pack_service_server::PackServiceServer::new(svc.clone()))
|
|
.add_service(tag_service_server::TagServiceServer::new(svc.clone()))
|
|
.add_service(tree_service_server::TreeServiceServer::new(svc));
|
|
tracing::info!("server ready, starting to accept connections");
|
|
server.serve(addr).await
|
|
}
|