194c44c
use std::collections::BTreeMap;
use std::path::PathBuf;
use gitbaby::repo::RepositoryFacade;
use gix::actor::Signature;
use gix::bstr::BString;
use gix::date::Time;
use gix::objs::Commit;
use gix::objs::Write;
use gix::{ObjectId, objs::Kind};
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use crate::service::GitService;
use crate::share::errors::{GitError, map_baby};
use crate::share::identity::load_identity;
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct SignatureDto {
pub name: String,
pub email: String,
pub when: OffsetDateTime,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "lowercase")]
pub enum ObjectKind {
Merge,
Squash,
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct FileEntryDto {
pub path: String,
pub content: Vec<u8>,
pub executable: bool,
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct WriteObjectResultDto {
pub id: String,
}
fn gix_signature(sig: &SignatureDto, offset_seconds: i32) -> Signature {
Signature {
name: BString::from(sig.name.clone()),
email: BString::from(sig.email.clone()),
time: Time {
seconds: sig.when.unix_timestamp(),
offset: offset_seconds,
},
}
}
impl GitService {
pub async fn caller_signature(
&self,
caller: i64,
when: OffsetDateTime,
) -> Result<SignatureDto, GitError> {
let ident = load_identity(&self.state.database, caller).await?;
Ok(SignatureDto {
name: ident.name,
email: ident.email,
when,
})
}
/// Write a blob object (no ref update).
pub async fn write_blob(
&self,
space_name: String,
repo_name: String,
data: Vec<u8>,
) -> Result<WriteObjectResultDto, GitError> {
if data.len() as u64 > self.state.config.git.max_blob_size {
return Err(GitError::InvalidArgument(
"blob exceeds max size".to_string(),
));
}
let ctx = self.context(space_name, repo_name).await?;
let repo = ctx.gix_repo().await.map_err(GitError::from)?;
let id = repo
.write_blob(data)
.map_err(|e| GitError::Internal(format!("write blob failed: {e}")))?;
Ok(WriteObjectResultDto { id: id.to_string() })
}
/// Build a tree object from flat file entries (recursive dirs created
/// implicitly). Returns the new tree id without touching any ref.
pub async fn write_tree_from_files(
&self,
space_name: String,
repo_name: String,
files: Vec<FileEntryDto>,
) -> Result<WriteObjectResultDto, GitError> {
let ctx = self.context(space_name, repo_name).await?;
let repo = ctx.gix_repo().await.map_err(GitError::from)?;
let mut blobs: BTreeMap<String, ObjectId> = BTreeMap::new();
for f in &files {
let path = clean_rel_path(&f.path)?;
let id = repo
.write_blob(&f.content)
.map_err(|e| GitError::Internal(format!("write blob `{path}` failed: {e}")))?;
blobs.insert(path, id.into());
}
let tree_oid = build_tree_recursive(&repo, "", &blobs)?;
Ok(WriteObjectResultDto {
id: tree_oid.to_string(),
})
}
/// Write a commit object (merge = arbitrary parents, squash = one
/// parent). Does not move any ref or HEAD.
#[allow(clippy::too_many_arguments)]
pub async fn commit_object(
&self,
space_name: String,
repo_name: String,
kind: ObjectKind,
parents: Vec<String>,
tree: String,
message: String,
author: SignatureDto,
committer: SignatureDto,
) -> Result<WriteObjectResultDto, GitError> {
if parents.is_empty() {
return Err(GitError::InvalidArgument(
"commit requires at least one parent".to_string(),
));
}
let ctx = self.context(space_name, repo_name).await?;
let baby = self.baby(&ctx);
let parent_oids: Vec<gix::ObjectId> = match kind {
ObjectKind::Merge => {
let mut oids = Vec::with_capacity(parents.len());
for p in &parents {
oids.push(baby.rev_parse(p).await.map_err(map_baby)?);
}
oids
}
ObjectKind::Squash => {
vec![baby.rev_parse(&parents[0]).await.map_err(map_baby)?]
}
};
let tree_id = gix::ObjectId::from_hex(tree.trim().as_bytes())
.map_err(|_| GitError::InvalidArgument("invalid tree id".to_string()))?;
let repo = ctx.gix_repo().await.map_err(GitError::from)?;
let commit = Commit {
tree: tree_id,
parents: parent_oids.into(),
author: gix_signature(&author, 0),
committer: gix_signature(&committer, 0),
encoding: None,
message: BString::from(message.clone()),
extra_headers: Vec::new(),
};
let id = repo
.write_object(&commit)
.map_err(|e| GitError::Internal(format!("write commit failed: {e}")))?;
Ok(WriteObjectResultDto { id: id.to_string() })
}
}
fn clean_rel_path(path: &str) -> Result<String, GitError> {
let p = PathBuf::from(path);
if p.is_absolute()
|| p.components()
.any(|c| matches!(c, std::path::Component::ParentDir))
|| path.contains('\\')
|| path.chars().any(|c| c.is_control())
{
return Err(GitError::InvalidArgument(format!(
"invalid relative path `{path}`"
)));
}
Ok(p.to_string_lossy().replace('\\', "/"))
}
fn tree_entry_bytes(mode_octal: u32, name: &str, oid: ObjectId) -> Vec<u8> {
let mut buf = Vec::with_capacity(64 + name.len());
use std::io::Write as _;
let _ = write!(&mut buf, "{:o} ", mode_octal);
buf.extend_from_slice(name.as_bytes());
buf.push(0);
buf.extend_from_slice(oid.as_bytes());
buf
}
fn build_tree_recursive_sync(
prefix: &str,
blobs: &BTreeMap<String, ObjectId>,
write_tree: &mut dyn FnMut(&[u8]) -> Result<ObjectId, GitError>,
) -> Result<ObjectId, GitError> {
// Group blobs by their first path segment relative to `prefix`.
let mut direct: BTreeMap<String, ObjectId> = BTreeMap::new();
let mut dir_blobs: BTreeMap<String, BTreeMap<String, ObjectId>> = BTreeMap::new();
for (path, oid) in blobs {
if !path.starts_with(prefix) {
continue;
}
let rest = &path[prefix.len()..];
if rest.is_empty() {
continue;
}
let head = rest.split('/').next().unwrap_or_default();
if rest == head {
direct.insert(head.to_string(), *oid);
} else {
let _dir = format!("{prefix}{head}/");
let entry = dir_blobs.entry(head.to_string()).or_default();
entry.insert(path.clone(), *oid);
}
}
let mut entries: Vec<(String, ObjectId)> = Vec::new();
for (name, oid) in &direct {
entries.push((name.clone(), *oid));
}
for (dir_name, sub_blobs) in &dir_blobs {
let sub_prefix = format!("{prefix}{dir_name}/");
let sub_oid = build_tree_recursive_sync(&sub_prefix, sub_blobs, write_tree)?;
entries.push((format!("{dir_name}/"), sub_oid));
}
entries.sort_by(|a, b| a.0.cmp(&b.0));
let mut buf: Vec<u8> = Vec::new();
for (name, oid) in entries {
let (mode, name) = if let Some(plain) = name.strip_suffix('/') {
(0o040000u32, plain)
} else {
(0o100644u32, name.as_str())
};
buf.extend_from_slice(&tree_entry_bytes(mode, name, oid));
}
let mut object = format!("tree {}\0", buf.len()).into_bytes();
object.extend_from_slice(&buf);
write_tree(&object)
}
fn build_tree_recursive(
repo: &gix::Repository,
prefix: &str,
blobs: &BTreeMap<String, ObjectId>,
) -> Result<ObjectId, GitError> {
let objects = repo.objects.clone();
let mut write_tree = move |data: &[u8]| -> Result<ObjectId, GitError> {
objects
.write_buf(Kind::Tree, data)
.map_err(|e| GitError::Internal(format!("write tree failed: {e}")))
};
build_tree_recursive_sync(prefix, blobs, &mut write_tree)
}