2ae9196
ZhenYi
feat(lib/git): add HTTP handler tree with routes and errors across analysis, archives, blobs, branches, commits, conflicts, content, diffs, hooks, lfs, logs, maintenance, packs, patches, pipelines, refs, remotes, repos, signatures, submodules, tags, trees
2ae9196
use std::path::PathBuf;
use axum::body::Body;
use axum::http::{HeaderMap, StatusCode, header};
use axum::response::{IntoResponse, Response};
use bytes::Bytes;
use flate2::read::GzDecoder;
use std::io::Read;
use uuid::Uuid;
use crate::context::facade::GitContext;
use crate::share::access::{BasicAuth, allow_repo_read, require_writer};
use crate::smart::error::{SmartError, SmartResult};
use crate::smart::info_refs as info_refs_module;
use crate::smart::pre_receive::{self, parse_receive_pack_refs};
use crate::smart::rpc::{self, GitService};
use crate::smart::service::SmartService;
use crate::sync::post_receive::enqueue_post_receive_push;
const MAX_RPC_BODY: usize = 512 * 1024 * 1024;
pub async fn info_refs(
service: &SmartService,
space: &str,
repo: &str,
_headers: &HeaderMap,
auth: &BasicAuth,
service_kind: Option<&str>,
) -> SmartResult<Response> {
let (ctx, _actor_id) = match service_kind {
Some(name) => {
let git_service = GitService::from_name(name)?;
let want_write = matches!(git_service, GitService::ReceivePack);
let ctx = service.resolve(space, repo).await?;
let actor_id = authorize(service, &ctx, auth, want_write).await?;
(ctx, actor_id)
}
None => {
let ctx = service.resolve(space, repo).await?;
allow_repo_read(&service.git, auth, &ctx).await?;
(ctx, None)
}
};
let bare = service.bare_path(&ctx);
match service_kind {
Some(name) => {
let git_service = GitService::from_name(name)?;
info_refs_module::smart_advertisement(&bare, git_service).await
}
None => info_refs_module::dumb_info_refs(&bare).await,
}
}
#[allow(clippy::too_many_arguments)]
pub async fn rpc(
service: &SmartService,
space: &str,
repo: &str,
headers: &HeaderMap,
body: Body,
auth: &BasicAuth,
git_service: GitService,
) -> SmartResult<Response> {
let want_write = matches!(git_service, GitService::ReceivePack);
let ctx = service.resolve(space, repo).await?;
let actor_id = authorize(service, &ctx, auth, want_write).await?;
let bare = service.bare_path(&ctx);
let space_id = service.owning_space_id(&ctx).await?;
let run = async {
let input = decode_request_body(headers, body, MAX_RPC_BODY).await?;
let is_receive = git_service == GitService::ReceivePack;
if is_receive && actor_id.is_some() {
let updates = parse_receive_pack_refs(&input)?;
if !updates.is_empty()
&& let Some(actor_uid) = actor_id {
let baby = service.git.baby(&ctx);
let verdict = pre_receive::validate_pre_receive(
service.git.db_read(),
&baby,
ctx.model.id,
actor_uid,
&updates,
)
.await?;
verdict.accepted_only()?;
}
if let Err(e) = crate::sync::post_receive::prepare_post_receive(&bare) {
tracing::warn!(?e, "post-receive hook setup failed");
}
}
let output = rpc::run_rpc_bytes(&bare, git_service, false, input).await?;
if is_receive
&& let Some(queue) = &service.queue
{
match enqueue_post_receive_push(
queue,
&service.sync_settings,
space_id,
ctx.model.id,
actor_id,
&bare,
)
.await
{
Ok(n) if n > 0 => {
tracing::debug!(count = n, "enqueued post-receive ref updates")
}
Ok(_) => tracing::debug!("no refs to enqueue"),
Err(e) => tracing::warn!(?e, "post-receive enqueue failed"),
}
}
Ok::<_, SmartError>(
(
StatusCode::OK,
[
(header::CONTENT_TYPE, git_service.result_content_type()),
(header::CACHE_CONTROL, "no-cache"),
],
output,
)
.into_response(),
)
};
match git_service {
GitService::ReceivePack => {
let key = format!("push:{}:{}", space_id, ctx.model.id);
service.push_coordinator().run(&key, run).await
}
GitService::UploadPack => run.await,
}
}
async fn authorize(
service: &SmartService,
ctx: &crate::context::facade::GitContext,
auth: &BasicAuth,
want_write: bool,
) -> SmartResult<Option<Uuid>> {
let svc = &service.git;
if want_write {
let uid = require_writer(svc, auth, ctx).await?;
Ok(Some(uid))
} else {
let uid = allow_repo_read(svc, auth, ctx).await?;
Ok(uid)
}
}
pub async fn decode_request_body(
headers: &HeaderMap,
body: Body,
limit: usize,
) -> SmartResult<Bytes> {
let bytes = axum::body::to_bytes(body, limit)
.await
.map_err(|e| SmartError::BadRequest(format!("failed to read request body: {e}")))?;
let encoding = headers
.get(header::CONTENT_ENCODING)
.and_then(|v| v.to_str().ok())
.map(|v| v.to_ascii_lowercase());
if encoding.as_deref() == Some("gzip") {
let decoder = GzDecoder::new(bytes.as_ref());
let mut out = Vec::new();
let cap = limit as u64 + 1;
decoder
.take(cap)
.read_to_end(&mut out)
.map_err(|e| SmartError::BadRequest(format!("failed to decompress gzip body: {e}")))?;
if out.len() as u64 > limit as u64 {
return Err(SmartError::BadRequest(format!(
"decompressed body exceeds limit of {limit} bytes"
)));
}
Ok(Bytes::from(out))
} else {
Ok(bytes)
}
}
#[allow(dead_code)]
fn _unused(_ctx: &GitContext, _p: &PathBuf) {}