feat(server): add tracing spans and caching to archive and blame services

- 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
This commit is contained in:
zhenyi
2026-06-04 15:33:16 +08:00
parent 729604f13b
commit cc202d6d1f
41 changed files with 2400 additions and 1067 deletions
Generated
+1
View File
@@ -503,6 +503,7 @@ dependencies = [
name = "gitks" name = "gitks"
version = "1.0.0" version = "1.0.0"
dependencies = [ dependencies = [
"clru",
"dotenvy", "dotenvy",
"duct", "duct",
"gix", "gix",
+1
View File
@@ -16,6 +16,7 @@ documentation = ""
path = "lib.rs" path = "lib.rs"
name = "gitks" name = "gitks"
[dependencies] [dependencies]
clru = "0.6.3"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
gix = { version = "0.84.0", default-features = false, features = ["serde", "blame", "sha256", "sha1", "tracing", "merge", "max-performance-safe", "revision"] } gix = { version = "0.84.0", default-features = false, features = ["serde", "blame", "sha256", "sha1", "tracing", "merge", "max-performance-safe", "revision"] }
gix-archive = { version = "0.33.0", features = ["sha256","sha1","document-features"] } gix-archive = { version = "0.33.0", features = ["sha256","sha1","document-features"] }
+82 -14
View File
@@ -1,11 +1,27 @@
use std::process::Command; use std::process::{Command, Stdio};
use tokio_stream::wrappers::ReceiverStream;
use crate::bare::GitBare; use crate::bare::GitBare;
use crate::error::{GitError, GitResult};
use crate::pb::{ArchiveChunk, ArchiveRequest, archive_options, object_selector}; use crate::pb::{ArchiveChunk, ArchiveRequest, archive_options, object_selector};
impl GitBare { impl GitBare {
pub fn get_archive(&self, request: ArchiveRequest) -> GitResult<Vec<ArchiveChunk>> { /// Stream archive data via a channel to avoid loading the entire archive into memory.
/// Returns a ReceiverStream that yields ArchiveChunk as the subprocess produces output.
pub fn get_archive_stream(
&self,
request: ArchiveRequest,
) -> Result<ReceiverStream<Result<ArchiveChunk, tonic::Status>>, tonic::Status> {
let bare_dir = self.bare_dir.clone();
tracing::info!(
repo = %bare_dir.display(),
"spawning git archive subprocess"
);
let (tx, rx) = tokio::sync::mpsc::channel(16);
// Spawn the blocking git subprocess in a dedicated thread
tokio::task::spawn_blocking(move || {
let revision = match request.treeish.and_then(|s| s.selector) { let revision = match request.treeish.and_then(|s| s.selector) {
Some(object_selector::Selector::Oid(oid)) => oid.hex, Some(object_selector::Selector::Oid(oid)) => oid.hex,
Some(object_selector::Selector::Revision(name)) => name.revision, Some(object_selector::Selector::Revision(name)) => name.revision,
@@ -27,19 +43,71 @@ impl GitBare {
args.push("--".into()); args.push("--".into());
args.extend(options.pathspec); args.extend(options.pathspec);
} }
let output = Command::new("git")
let mut child = match Command::new("git")
.arg("--git-dir") .arg("--git-dir")
.arg(&self.bare_dir) .arg(&bare_dir)
.args(&args) .args(&args)
.output()?; .stdout(Stdio::piped())
if !output.status.success() { .stderr(Stdio::piped())
return Err(GitError::CommandFailed { .spawn()
status_code: output.status.code(), {
stderr: String::from_utf8_lossy(&output.stderr).into_owned(), Ok(c) => c,
}); Err(e) => {
let _ = tx.blocking_send(Err(tonic::Status::internal(format!(
"failed to spawn git archive: {e}"
))));
return;
} }
Ok(vec![ArchiveChunk { };
data: output.stdout,
}]) let stdout = match child.stdout.take() {
Some(s) => s,
None => {
let _ =
tx.blocking_send(Err(tonic::Status::internal("failed to capture stdout")));
return;
}
};
// Read stdout in 64KB chunks and stream them
use std::io::Read;
let mut reader = std::io::BufReader::new(stdout);
let mut buf = vec![0u8; 65536];
loop {
match reader.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
let chunk = ArchiveChunk {
data: buf[..n].to_vec(),
};
if tx.blocking_send(Ok(chunk)).is_err() {
break;
}
}
Err(e) => {
let _ = tx.blocking_send(Err(tonic::Status::internal(format!(
"read error: {e}"
))));
break;
}
}
}
match child.wait() {
Ok(status) if !status.success() => {
let _ = tx.blocking_send(Err(tonic::Status::internal(
"git archive exited with error",
)));
}
Err(e) => {
let _ =
tx.blocking_send(Err(tonic::Status::internal(format!("wait error: {e}"))));
}
_ => {}
}
});
Ok(ReceiverStream::new(rx))
} }
} }
+20 -2
View File
@@ -3,14 +3,24 @@ use std::path::{Path, PathBuf};
use crate::error::{GitError, GitResult}; use crate::error::{GitError, GitResult};
use crate::pb::RepositoryHeader; use crate::pb::RepositoryHeader;
#[derive(Debug)]
pub struct GitBare { pub struct GitBare {
pub bare_dir: PathBuf, pub bare_dir: PathBuf,
} }
impl GitBare { impl GitBare {
pub fn new(bare_dir: PathBuf) -> Self {
Self { bare_dir }
}
/// Open the gix repository. Callers should open once per logical operation
/// and reuse the handle for all gix lookups within that operation.
pub fn gix_repo(&self) -> GitResult<gix::Repository> { pub fn gix_repo(&self) -> GitResult<gix::Repository> {
gix::open(&self.bare_dir) tracing::debug!(repo = %self.bare_dir.display(), "opening gix repository");
.map_err(|e| GitError::Internal(format!("failed to open gix repository: {e}"))) gix::open(&self.bare_dir).map_err(|e| {
tracing::error!(repo = %self.bare_dir.display(), error = %e, "failed to open gix repository");
GitError::Internal(format!("failed to open gix repository: {e}"))
})
} }
pub fn from_repository_header(header: &RepositoryHeader) -> GitResult<Self> { pub fn from_repository_header(header: &RepositoryHeader) -> GitResult<Self> {
@@ -47,6 +57,11 @@ impl GitBare {
// Path traversal check: canonical resolved dir must start with base // Path traversal check: canonical resolved dir must start with base
let base_canon = base.canonicalize().unwrap_or_else(|_| base.clone()); let base_canon = base.canonicalize().unwrap_or_else(|_| base.clone());
if !canonical.starts_with(&base_canon) { if !canonical.starts_with(&base_canon) {
tracing::warn!(
relative_path = %relative_path,
base = %base_canon.display(),
"path traversal attempt detected"
);
return Err(GitError::InvalidArgument(format!( return Err(GitError::InvalidArgument(format!(
"path traversal detected: {relative_path} escapes storage root" "path traversal detected: {relative_path} escapes storage root"
))); )));
@@ -60,6 +75,7 @@ impl GitBare {
// Validate bare_dir exists, is a directory, and is readable // Validate bare_dir exists, is a directory, and is readable
if !bare_dir.exists() { if !bare_dir.exists() {
tracing::warn!(path = %bare_dir.display(), "repository not found");
return Err(GitError::RepoNotFound); return Err(GitError::RepoNotFound);
} }
if !bare_dir.is_dir() { if !bare_dir.is_dir() {
@@ -75,6 +91,7 @@ impl GitBare {
// Maybe it's a non-bare repo // Maybe it's a non-bare repo
let git_dir = bare_dir.join(".git"); let git_dir = bare_dir.join(".git");
if git_dir.is_dir() && git_dir.join("HEAD").exists() { if git_dir.is_dir() && git_dir.join("HEAD").exists() {
tracing::debug!(path = %git_dir.display(), "resolved non-bare repo via .git subdir");
return Ok(Self { bare_dir: git_dir }); return Ok(Self { bare_dir: git_dir });
} }
return Err(GitError::NotBareRepository); return Err(GitError::NotBareRepository);
@@ -87,6 +104,7 @@ impl GitBare {
pub fn object_format(&self) -> crate::pb::ObjectFormat { pub fn object_format(&self) -> crate::pb::ObjectFormat {
let repo = self.gix_repo().ok(); let repo = self.gix_repo().ok();
let kind = repo let kind = repo
.as_ref()
.map(|r| r.object_hash()) .map(|r| r.object_hash())
.unwrap_or(gix::hash::Kind::Sha1); .unwrap_or(gix::hash::Kind::Sha1);
match kind { match kind {
+8 -2
View File
@@ -5,10 +5,16 @@ use crate::pb::{BlameHunk, BlameLine, BlameRequest, BlameResponse, PageInfo};
impl GitBare { impl GitBare {
pub fn blame(&self, request: BlameRequest) -> GitResult<BlameResponse> { pub fn blame(&self, request: BlameRequest) -> GitResult<BlameResponse> {
let revision = match request.revision.and_then(|s| s.selector) { let revision = match request.revision.and_then(|s| s.selector) {
Some(crate::pb::object_selector::Selector::Oid(oid)) => oid.hex, Some(crate::pb::object_selector::Selector::Oid(oid)) => oid.hex.clone(),
Some(crate::pb::object_selector::Selector::Revision(name)) => name.revision, Some(crate::pb::object_selector::Selector::Revision(name)) => name.revision.clone(),
None => "HEAD".into(), None => "HEAD".into(),
}; };
tracing::info!(
repo = %self.bare_dir.display(),
path = %request.path,
revision = %revision,
"running blame"
);
let mut args = vec![ let mut args = vec![
"--git-dir".to_string(), "--git-dir".to_string(),
self.bare_dir.to_string_lossy().into_owned(), self.bare_dir.to_string_lossy().into_owned(),
+72 -22
View File
@@ -1,10 +1,8 @@
use crate::bare::GitBare; use crate::bare::GitBare;
use crate::commit::list_commits::read_commit_from_repo;
use crate::diff::get_diff_stats::diff_stats_for_range; use crate::diff::get_diff_stats::diff_stats_for_range;
use crate::error::{GitError, GitResult}; use crate::error::{GitError, GitResult};
use crate::paginate; use crate::pb::{CommitStats, CompareCommitsRequest, CompareCommitsResponse, object_selector};
use crate::pb::{
CommitStats, CompareCommitsRequest, CompareCommitsResponse, GetCommitRequest, object_selector,
};
impl GitBare { impl GitBare {
pub fn compare_commits( pub fn compare_commits(
@@ -35,17 +33,66 @@ impl GitBare {
} else { } else {
format!("{base}...{head}") format!("{base}...{head}")
}; };
let mut args = vec![
// Build base rev-list args
let mut base_args = vec![
"--git-dir".to_string(), "--git-dir".to_string(),
self.bare_dir.to_string_lossy().into_owned(), self.bare_dir.to_string_lossy().into_owned(),
"rev-list".into(), "rev-list".into(),
]; ];
if request.first_parent { if request.first_parent {
args.push("--first-parent".into()); base_args.push("--first-parent".into());
} }
args.push(range); base_args.push(range);
let rev_list = duct::cmd("git", &args) // 1. Total count
let total = {
let mut args = base_args.clone();
// Insert after "rev-list" (index 2)
args.insert(3, "--count".into());
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(),
});
}
String::from_utf8_lossy(&result.stdout)
.trim()
.parse::<usize>()
.unwrap_or(0)
};
// 2. Git-side pagination
let page_size = request
.pagination
.as_ref()
.map(|p| p.page_size as usize)
.unwrap_or(total.max(1))
.max(1);
let start_offset = request
.pagination
.as_ref()
.and_then(|p| {
if p.page_token.is_empty() {
None
} else {
p.page_token.parse::<usize>().ok()
}
})
.unwrap_or(0)
.min(total);
let mut fetch_args = base_args;
// Insert after "rev-list" (index 2)
fetch_args.insert(3, format!("--skip={start_offset}"));
fetch_args.insert(4, format!("-n{page_size}"));
let rev_list = duct::cmd("git", &fetch_args)
.stdout_capture() .stdout_capture()
.stderr_capture() .stderr_capture()
.unchecked() .unchecked()
@@ -57,28 +104,31 @@ impl GitBare {
}); });
} }
let ids = String::from_utf8_lossy(&rev_list.stdout) let page_ids: Vec<String> = String::from_utf8_lossy(&rev_list.stdout)
.lines() .lines()
.map(str::trim) .map(str::trim)
.filter(|line| !line.is_empty()) .filter(|line| !line.is_empty())
.map(ToOwned::to_owned) .map(ToOwned::to_owned)
.collect::<Vec<_>>(); .collect();
let (page_ids, page_info) = paginate::paginate(&ids, request.pagination.as_ref());
// 3. Batch-read commits via gix (one repo open, no subprocess per commit)
let mut commits = Vec::with_capacity(page_ids.len()); let mut commits = Vec::with_capacity(page_ids.len());
for id in page_ids { for id in &page_ids {
commits.push(self.get_commit(GetCommitRequest { commits.push(read_commit_from_repo(self, &repo, id)?);
repository: request.repository.clone(),
revision: Some(crate::pb::ObjectSelector {
selector: Some(object_selector::Selector::Revision(crate::pb::ObjectName {
revision: id,
})),
}),
include_stats: false,
include_raw: false,
})?);
} }
let end = start_offset + page_ids.len();
let has_next = end < total;
let page_info = crate::pb::PageInfo {
next_page_token: if has_next {
end.to_string()
} else {
String::new()
},
has_next_page: has_next,
total_count: total as u64,
};
let diff_stats = diff_stats_for_range(self, &base, &head, None)?; let diff_stats = diff_stats_for_range(self, &base, &head, None)?;
Ok(CompareCommitsResponse { Ok(CompareCommitsResponse {
commits, commits,
+7
View File
@@ -9,6 +9,13 @@ use crate::pb::{
impl GitBare { impl GitBare {
pub fn create_commit(&self, request: CreateCommitRequest) -> GitResult<CreateCommitResponse> { pub fn create_commit(&self, request: CreateCommitRequest) -> GitResult<CreateCommitResponse> {
let repo = self.gix_repo()?; let repo = self.gix_repo()?;
let branch = request.branch.clone();
tracing::debug!(
repo = %self.bare_dir.display(),
branch = %branch,
actions = request.actions.len(),
"creating commit"
);
let start_rev = match request.start_revision.clone().and_then(|s| s.selector) { let start_rev = match request.start_revision.clone().and_then(|s| s.selector) {
Some(object_selector::Selector::Oid(oid)) => oid.hex, Some(object_selector::Selector::Oid(oid)) => oid.hex,
Some(object_selector::Selector::Revision(name)) => name.revision, Some(object_selector::Selector::Revision(name)) => name.revision,
+159 -44
View File
@@ -1,7 +1,6 @@
use crate::bare::GitBare; use crate::bare::GitBare;
use crate::error::{GitError, GitResult}; use crate::error::{GitError, GitResult};
use crate::paginate; use crate::pb::{Commit, ListCommitsRequest, ListCommitsResponse, object_selector};
use crate::pb::{GetCommitRequest, ListCommitsRequest, ListCommitsResponse, object_selector};
impl GitBare { impl GitBare {
pub fn list_commits(&self, request: ListCommitsRequest) -> GitResult<ListCommitsResponse> { pub fn list_commits(&self, request: ListCommitsRequest) -> GitResult<ListCommitsResponse> {
@@ -11,9 +10,109 @@ impl GitBare {
None => "HEAD".into(), None => "HEAD".into(),
}; };
let base_args = build_rev_list_args(self, &request, &revision);
// 1. Get total count via rev-list --count (lightweight, no object parsing)
let total = {
let mut args = base_args.clone();
// Insert after "rev-list" (index 2) so it's a rev-list flag, not a git flag
args.insert(3, "--count".into());
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(),
});
}
String::from_utf8_lossy(&result.stdout)
.trim()
.parse::<usize>()
.unwrap_or(0)
};
// 2. Apply git-side pagination: --skip + -n to only fetch the page
let page_size = request
.pagination
.as_ref()
.map(|p| p.page_size as usize)
.unwrap_or(total.max(1))
.max(1);
let start_offset = request
.pagination
.as_ref()
.and_then(|p| {
if p.page_token.is_empty() {
None
} else {
p.page_token.parse::<usize>().ok()
}
})
.unwrap_or(0)
.min(total);
let mut fetch_args = base_args;
// Insert after "rev-list" (index 2) so they are rev-list flags, not git flags
fetch_args.insert(3, format!("--skip={start_offset}"));
fetch_args.insert(4, format!("-n{page_size}"));
let result = duct::cmd("git", &fetch_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(),
});
}
let page_ids: Vec<String> = String::from_utf8_lossy(&result.stdout)
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.map(ToOwned::to_owned)
.collect();
// 3. Batch-read commits via gix (one repo open, zero subprocess per commit)
let commits = if page_ids.is_empty() {
Vec::new()
} else {
let repo = self.gix_repo()?;
let mut commits = Vec::with_capacity(page_ids.len());
for id in &page_ids {
commits.push(read_commit_from_repo(self, &repo, id)?);
}
commits
};
let end = start_offset + page_ids.len();
let has_next = end < total;
let page_info = crate::pb::PageInfo {
next_page_token: if has_next {
end.to_string()
} else {
String::new()
},
has_next_page: has_next,
total_count: total as u64,
};
Ok(ListCommitsResponse {
commits,
page_info: Some(page_info),
})
}
}
fn build_rev_list_args(gb: &GitBare, request: &ListCommitsRequest, revision: &str) -> Vec<String> {
let mut args = vec![ let mut args = vec![
"--git-dir".to_string(), "--git-dir".to_string(),
self.bare_dir.to_string_lossy().into_owned(), gb.bare_dir.to_string_lossy().into_owned(),
"rev-list".into(), "rev-list".into(),
]; ];
if request.first_parent { if request.first_parent {
@@ -37,50 +136,66 @@ impl GitBare {
if request.all { if request.all {
args.push("--all".into()); args.push("--all".into());
} else { } else {
args.push(revision); args.push(revision.to_string());
} }
if !request.path.is_empty() { if !request.path.is_empty() {
args.push("--".into()); args.push("--".into());
args.push(request.path.clone()); args.push(request.path.clone());
} }
args
let result = duct::cmd("git", &args) }
.stdout_capture()
.stderr_capture() /// Read a single commit from an already-opened gix repo (no subprocess).
.unchecked() pub(crate) fn read_commit_from_repo(
.run()?; gb: &GitBare,
if !result.status.success() { repo: &gix::Repository,
return Err(GitError::CommandFailed { hex: &str,
status_code: result.status.code(), ) -> GitResult<Commit> {
stderr: String::from_utf8_lossy(&result.stderr).into_owned(), let id = gix::hash::ObjectId::from_hex(hex.as_bytes())
}); .map_err(|e| crate::error::GitError::InvalidOid(e.to_string()))?;
} let commit = repo
.find_object(id)?
let ids = String::from_utf8_lossy(&result.stdout) .try_into_commit()
.lines() .map_err(|e| crate::error::GitError::Gix(e.to_string()))?;
.map(str::trim) let tree_hex = commit.tree_id()?.to_string();
.filter(|line| !line.is_empty()) let message = commit.message_raw()?.to_string();
.map(ToOwned::to_owned) let (subject, body) = message
.collect::<Vec<_>>(); .split_once('\n')
let (page_ids, page_info) = paginate::paginate(&ids, request.pagination.as_ref()); .map(|(s, b)| (s.to_string(), b.trim_start_matches('\n').to_string()))
.unwrap_or_else(|| (message.clone(), String::new()));
let mut commits = Vec::with_capacity(page_ids.len()); let author_sig = commit.author().ok();
for id in page_ids { let committer_sig = commit.committer().ok();
commits.push(self.get_commit(GetCommitRequest { Ok(Commit {
repository: request.repository.clone(), oid: Some(gb.oid_to_pb(hex.to_string())),
revision: Some(crate::pb::ObjectSelector { abbreviated_oid: commit
selector: Some(object_selector::Selector::Revision(crate::pb::ObjectName { .short_id()
revision: id, .map(|s| s.to_string())
})), .unwrap_or_else(|_| hex.chars().take(7).collect()),
}), parent_oids: commit
include_stats: false, .parent_ids()
include_raw: false, .map(|p| gb.oid_to_pb(p.to_string()))
})?); .collect(),
} tree_oid: Some(gb.oid_to_pb(tree_hex)),
author: author_sig
Ok(ListCommitsResponse { .as_ref()
commits, .map(crate::commit::get_commit::gix_sig_to_pb),
page_info: Some(page_info), committer: committer_sig
}) .as_ref()
} .map(crate::commit::get_commit::gix_sig_to_pb),
subject,
body,
message,
trailers: Vec::new(),
signature: None,
stats: None,
authored_at: author_sig.as_ref().map(|s| prost_types::Timestamp {
seconds: s.seconds(),
nanos: 0,
}),
committed_at: committer_sig.as_ref().map(|s| prost_types::Timestamp {
seconds: s.seconds(),
nanos: 0,
}),
raw: Vec::new(),
})
} }
+244 -183
View File
@@ -1,3 +1,5 @@
use std::collections::HashMap;
use crate::bare::GitBare; use crate::bare::GitBare;
use crate::diff::get_diff_stats::{diff_stats_for_range, push_diff_options}; use crate::diff::get_diff_stats::{diff_stats_for_range, push_diff_options};
use crate::error::{GitError, GitResult}; use crate::error::{GitError, GitResult};
@@ -5,62 +7,106 @@ use crate::paginate;
use crate::pb::diff_file::ChangeType; use crate::pb::diff_file::ChangeType;
use crate::pb::{DiffFile, GetDiffRequest, GetDiffResponse}; use crate::pb::{DiffFile, GetDiffRequest, GetDiffResponse};
#[derive(Debug, Clone)] /// Parsed entry from `git diff --raw -z`
struct NameStatusEntry { struct RawDiffEntry {
status: char, status: char,
old_path: String, old_path: String,
new_path: String, new_path: String,
old_mode: u32,
new_mode: u32,
old_oid: String,
new_oid: String,
similarity: f64, similarity: f64,
} }
#[derive(Debug, Clone, Default)]
struct TreeMeta {
oid_hex: String,
mode: u32,
}
impl GitBare { impl GitBare {
pub fn get_diff(&self, request: GetDiffRequest) -> GitResult<GetDiffResponse> { pub fn get_diff(&self, request: GetDiffRequest) -> GitResult<GetDiffResponse> {
let base = match request.base.and_then(|s| s.selector) { let base = match request.base.and_then(|s| s.selector) {
Some(crate::pb::object_selector::Selector::Oid(oid)) => oid.hex, Some(crate::pb::object_selector::Selector::Oid(oid)) => oid.hex.clone(),
Some(crate::pb::object_selector::Selector::Revision(name)) => name.revision, Some(crate::pb::object_selector::Selector::Revision(name)) => name.revision.clone(),
None => "HEAD".into(), None => "HEAD".into(),
}; };
let head = match request.head.and_then(|s| s.selector) { let head = match request.head.and_then(|s| s.selector) {
Some(crate::pb::object_selector::Selector::Oid(oid)) => oid.hex, Some(crate::pb::object_selector::Selector::Oid(oid)) => oid.hex.clone(),
Some(crate::pb::object_selector::Selector::Revision(name)) => name.revision, Some(crate::pb::object_selector::Selector::Revision(name)) => name.revision.clone(),
None => "HEAD".into(), None => "HEAD".into(),
}; };
tracing::debug!(
repo = %self.bare_dir.display(),
base = %base,
head = %head,
"computing diff"
);
let options = request.options.as_ref(); let options = request.options.as_ref();
let entries = self.diff_name_status(&base, &head, options)?; let want_patch = options.is_some_and(|o| o.include_patch);
let max_files = options.and_then(|o| (o.max_files > 0).then_some(o.max_files as usize));
let overflow = max_files.is_some_and(|max| entries.len() > max);
let entries_to_build =
max_files.map_or(entries.as_slice(), |max| &entries[..entries.len().min(max)]);
// ── Call 1: --raw -z --numstat -z (all metadata + line counts) ──
let (raw_entries, numstat_map) = self.diff_raw_and_numstat(&base, &head, options)?;
let max_files = options.and_then(|o| (o.max_files > 0).then_some(o.max_files as usize));
let overflow = max_files.is_some_and(|max| raw_entries.len() > max);
let entries_to_build = max_files.map_or(raw_entries.as_slice(), |max| {
&raw_entries[..raw_entries.len().min(max)]
});
// ── Call 2 (optional): --patch for all files at once ──
let patch_map = if want_patch {
self.diff_patch_batch(&base, &head, options)?
} else {
HashMap::new()
};
// ── Merge results (zero additional subprocess calls) ──
let mut files = Vec::with_capacity(entries_to_build.len()); let mut files = Vec::with_capacity(entries_to_build.len());
for entry in entries_to_build { for entry in entries_to_build {
let old_meta = if !entry.old_path.is_empty() { let path = if !entry.new_path.is_empty() {
self.tree_meta(&base, &entry.old_path).ok().flatten() &entry.new_path
} else { } else {
None &entry.old_path
}; };
let new_meta = if !entry.new_path.is_empty() { let (additions, deletions, binary) = numstat_map
self.tree_meta(&head, &entry.new_path).ok().flatten() .get(path)
.map(|(a, d, b)| (*a, *d, *b))
.unwrap_or((0, 0, false));
let too_large = options.is_some_and(|o| {
o.max_bytes > 0
&& patch_map
.get(path)
.is_some_and(|p: &Vec<u8>| p.len() > o.max_bytes as usize)
});
let patch = patch_map
.get(path)
.map(|p| {
let max = options.map(|o| o.max_bytes as usize).unwrap_or(0);
if too_large && max > 0 {
p[..max].to_vec()
} else { } else {
None p.clone()
}; }
let (additions, deletions, binary) = self.path_numstat(&base, &head, entry)?; })
let (patch, too_large) = self.path_patch(&base, &head, entry, options)?; .unwrap_or_default();
files.push(DiffFile { files.push(DiffFile {
old_path: entry.old_path.clone(), old_path: entry.old_path.clone(),
new_path: entry.new_path.clone(), new_path: entry.new_path.clone(),
old_oid: old_meta.as_ref().map(|m| self.oid_to_pb(&m.oid_hex)), old_oid: if !entry.old_oid.is_empty()
new_oid: new_meta.as_ref().map(|m| self.oid_to_pb(&m.oid_hex)), && entry.old_oid != "0000000000000000000000000000000000000000"
old_mode: old_meta.as_ref().map(|m| m.mode).unwrap_or(0), {
new_mode: new_meta.as_ref().map(|m| m.mode).unwrap_or(0), Some(self.oid_to_pb(&entry.old_oid))
} else {
None
},
new_oid: if !entry.new_oid.is_empty()
&& entry.new_oid != "0000000000000000000000000000000000000000"
{
Some(self.oid_to_pb(&entry.new_oid))
} else {
None
},
old_mode: entry.old_mode,
new_mode: entry.new_mode,
change_type: change_type(entry.status) as i32, change_type: change_type(entry.status) as i32,
binary, binary,
too_large, too_large,
@@ -72,6 +118,7 @@ impl GitBare {
}); });
} }
// ── Call 3: diff --shortstat (already efficient, single call) ──
let stats = diff_stats_for_range(self, &base, &head, options)?; let stats = diff_stats_for_range(self, &base, &head, options)?;
let (files, page_info) = paginate::paginate(&files, request.pagination.as_ref()); let (files, page_info) = paginate::paginate(&files, request.pagination.as_ref());
@@ -83,17 +130,25 @@ impl GitBare {
}) })
} }
fn diff_name_status( /// Single subprocess call that gets BOTH --raw and --numstat with -z.
/// Returns parsed raw entries and a map of path → (additions, deletions, binary).
///
/// Combined output format with -z (NUL-separated records):
/// :<src_mode> <dst_mode> <src_hash> <dst_hash> <status>\0<path>\0
/// (for R/C: ...\0<old_path>\0<new_path>\0)
/// Then numstat records: <add>\t<del>\t<path>\0
fn diff_raw_and_numstat(
&self, &self,
base: &str, base: &str,
head: &str, head: &str,
options: Option<&crate::pb::DiffOptions>, options: Option<&crate::pb::DiffOptions>,
) -> GitResult<Vec<NameStatusEntry>> { ) -> GitResult<(Vec<RawDiffEntry>, HashMap<String, (u32, u32, bool)>)> {
let mut args = vec![ let mut args = vec![
"--git-dir".to_string(), "--git-dir".to_string(),
self.bare_dir.to_string_lossy().into_owned(), self.bare_dir.to_string_lossy().into_owned(),
"diff".into(), "diff".into(),
"--name-status".into(), "--raw".into(),
"--numstat".into(),
"-z".into(), "-z".into(),
]; ];
push_diff_options(&mut args, options); push_diff_options(&mut args, options);
@@ -118,168 +173,140 @@ impl GitBare {
}); });
} }
let parts = result // Split by NUL — each record is NUL-terminated
.stdout let records: Vec<&[u8]> = result.stdout.split(|b| *b == 0).collect();
.split(|b| *b == 0)
.filter(|part| !part.is_empty())
.map(|part| String::from_utf8_lossy(part).into_owned())
.collect::<Vec<_>>();
let mut entries = Vec::new(); let mut raw_entries = Vec::new();
let mut idx = 0; let mut numstat_map: HashMap<String, (u32, u32, bool)> = HashMap::new();
while idx < parts.len() { let mut i = 0;
let status_token = &parts[idx];
idx += 1; while i < records.len() {
let status = status_token.chars().next().unwrap_or('M'); let record = records[i];
let similarity = status_token if record.is_empty() {
i += 1;
continue;
}
if record.starts_with(b":") {
// Raw meta record: ":<src_mode> <dst_mode> <src_hash> <dst_hash> <status_char>"
// In older git: tab before status. In newer git: space before status.
// The path(s) follow as separate NUL-terminated records.
let record_str = String::from_utf8_lossy(record).into_owned();
// Try tab separator first (older git), then space (newer git)
let (meta, status_str) = if let Some((m, s)) = record_str.rsplit_once('\t') {
(m, s)
} else if let Some((m, s)) = record_str.rsplit_once(' ') {
(m, s)
} else {
i += 1;
continue;
};
let meta_parts: Vec<&str> = meta.split_whitespace().collect();
let old_mode = meta_parts
.first()
.and_then(|s| u32::from_str_radix(s, 8).ok())
.unwrap_or(0);
let new_mode = meta_parts
.get(1)
.and_then(|s| u32::from_str_radix(s, 8).ok())
.unwrap_or(0);
let old_oid = meta_parts.get(2).unwrap_or(&"").to_string();
let new_oid = meta_parts.get(3).unwrap_or(&"").to_string();
let status = status_str.chars().next().unwrap_or('M');
let similarity = status_str
.get(1..) .get(1..)
.and_then(|s| s.parse::<f64>().ok()) .and_then(|s| s.parse::<f64>().ok())
.unwrap_or(0.0); .unwrap_or(0.0);
if matches!(status, 'R' | 'C') { // Read path record(s) that follow the meta record
if idx + 1 >= parts.len() {
break;
}
let old_path = parts[idx].clone();
let new_path = parts[idx + 1].clone();
idx += 2;
entries.push(NameStatusEntry {
status,
old_path,
new_path,
similarity,
});
} else {
if idx >= parts.len() {
break;
}
let path = parts[idx].clone();
idx += 1;
let (old_path, new_path) = match status { let (old_path, new_path) = match status {
'A' => (String::new(), path), 'R' | 'C' => {
'D' => (path, String::new()), // Two path records: old_path\0new_path\0
_ => (path.clone(), path), let op = if i + 1 < records.len() {
i += 1;
String::from_utf8_lossy(records[i]).into_owned()
} else {
String::new()
}; };
entries.push(NameStatusEntry { let np = if i + 1 < records.len() {
i += 1;
String::from_utf8_lossy(records[i]).into_owned()
} else {
String::new()
};
(op, np)
}
'A' => {
let p = if i + 1 < records.len() {
i += 1;
String::from_utf8_lossy(records[i]).into_owned()
} else {
String::new()
};
(String::new(), p)
}
'D' => {
let p = if i + 1 < records.len() {
i += 1;
String::from_utf8_lossy(records[i]).into_owned()
} else {
String::new()
};
(p, String::new())
}
_ => {
let p = if i + 1 < records.len() {
i += 1;
String::from_utf8_lossy(records[i]).into_owned()
} else {
String::new()
};
(p.clone(), p)
}
};
raw_entries.push(RawDiffEntry {
status, status,
old_path, old_path,
new_path, new_path,
old_mode,
new_mode,
old_oid,
new_oid,
similarity, similarity,
}); });
}
}
Ok(entries)
}
fn tree_meta(&self, revision: &str, path: &str) -> GitResult<Option<TreeMeta>> {
let result = duct::cmd(
"git",
[
"--git-dir",
self.bare_dir.to_string_lossy().as_ref(),
"ls-tree",
"-z",
"-l",
revision,
"--",
path,
],
)
.stdout_capture()
.stderr_capture()
.unchecked()
.run()?;
if !result.status.success() || result.stdout.is_empty() {
return Ok(None);
}
let record = result
.stdout
.split(|b| *b == 0)
.find(|part| !part.is_empty())
.map(|part| String::from_utf8_lossy(part).into_owned());
let Some(record) = record else {
return Ok(None);
};
let Some((meta, _path)) = record.split_once('\t') else {
return Ok(None);
};
let parts = meta.split_whitespace().collect::<Vec<_>>();
if parts.len() < 3 {
return Ok(None);
}
Ok(Some(TreeMeta {
mode: u32::from_str_radix(parts[0], 8).unwrap_or(0),
oid_hex: parts[2].to_string(),
}))
}
fn path_numstat(
&self,
base: &str,
head: &str,
entry: &NameStatusEntry,
) -> GitResult<(u32, u32, bool)> {
let path = if entry.new_path.is_empty() {
&entry.old_path
} else { } else {
&entry.new_path // Numstat record: "<add>\t<del>\t<path>"
}; let record_str = String::from_utf8_lossy(record);
let result = duct::cmd( let parts: Vec<&str> = record_str.split('\t').collect();
"git", if parts.len() >= 3 {
[ let binary = parts[0] == "-" || parts[1] == "-";
"--git-dir", let add = parts[0].parse().unwrap_or(0u32);
self.bare_dir.to_string_lossy().as_ref(), let del = parts[1].parse().unwrap_or(0u32);
"diff", let path = parts[2].to_string();
"--numstat", numstat_map.insert(path, (add, del, binary));
base,
head,
"--",
path,
],
)
.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(),
});
} }
let line = String::from_utf8_lossy(&result.stdout) }
.lines() i += 1;
.next()
.unwrap_or_default()
.to_string();
let mut parts = line.split('\t');
let add = parts.next().unwrap_or_default();
let del = parts.next().unwrap_or_default();
let binary = add == "-" || del == "-";
Ok((add.parse().unwrap_or(0), del.parse().unwrap_or(0), binary))
} }
fn path_patch( Ok((raw_entries, numstat_map))
}
/// Single subprocess call to get patches for ALL files at once.
/// Returns a map of path → patch bytes.
fn diff_patch_batch(
&self, &self,
base: &str, base: &str,
head: &str, head: &str,
entry: &NameStatusEntry,
options: Option<&crate::pb::DiffOptions>, options: Option<&crate::pb::DiffOptions>,
) -> GitResult<(Vec<u8>, bool)> { ) -> GitResult<HashMap<String, Vec<u8>>> {
let Some(options) = options else { let context = options
return Ok((Vec::new(), false)); .map(|o| o.context_lines.to_string())
}; .unwrap_or_else(|| "3".into());
if !options.include_patch {
return Ok((Vec::new(), false));
}
let path = if entry.new_path.is_empty() {
&entry.old_path
} else {
&entry.new_path
};
let context = options.context_lines.to_string();
let mut args = vec![ let mut args = vec![
"--git-dir".to_string(), "--git-dir".to_string(),
self.bare_dir.to_string_lossy().into_owned(), self.bare_dir.to_string_lossy().into_owned(),
@@ -287,14 +314,18 @@ impl GitBare {
"--patch".into(), "--patch".into(),
format!("--unified={context}"), format!("--unified={context}"),
]; ];
if options.include_binary { if options.is_some_and(|o| o.include_binary) {
args.push("--binary".into()); args.push("--binary".into());
} }
push_diff_options(&mut args, Some(options)); push_diff_options(&mut args, options);
args.push(base.to_string()); args.push(base.to_string());
args.push(head.to_string()); args.push(head.to_string());
if let Some(options) = options
&& !options.pathspec.is_empty()
{
args.push("--".into()); args.push("--".into());
args.push(path.to_string()); args.extend(options.pathspec.iter().cloned());
}
let result = duct::cmd("git", &args) let result = duct::cmd("git", &args)
.stdout_capture() .stdout_capture()
@@ -308,12 +339,42 @@ impl GitBare {
}); });
} }
let mut patch = result.stdout; // Split combined patch output by "diff --git" headers
let too_large = options.max_bytes > 0 && patch.len() > options.max_bytes as usize; let mut map = HashMap::new();
if too_large { let output = &result.stdout;
patch.truncate(options.max_bytes as usize); let header = b"diff --git ";
let mut chunks: Vec<&[u8]> = Vec::new();
let mut pos = 0;
// Find all header positions
let mut header_positions = Vec::new();
while let Some(idx) = output[pos..]
.windows(header.len())
.position(|w| w == header)
{
header_positions.push(pos + idx);
pos = pos + idx + header.len();
} }
Ok((patch, too_large))
for (i, &start) in header_positions.iter().enumerate() {
let end = header_positions.get(i + 1).copied().unwrap_or(output.len());
chunks.push(&output[start..end]);
}
for chunk in chunks {
// Extract file path from "diff --git a/path b/path\n"
let first_line_end = chunk
.iter()
.position(|&b| b == b'\n')
.unwrap_or(chunk.len());
let first_line = String::from_utf8_lossy(&chunk[..first_line_end]);
if let Some(b_pos) = first_line.rfind(" b/") {
let path = &first_line[b_pos + 3..];
map.insert(path.to_string(), chunk.to_vec());
}
}
Ok(map)
} }
} }
+5
View File
@@ -3,6 +3,11 @@ use crate::error::{GitError, GitResult};
impl GitBare { impl GitBare {
pub fn init_repository(&self, bare: bool) -> GitResult<()> { pub fn init_repository(&self, bare: bool) -> GitResult<()> {
tracing::info!(
path = %self.bare_dir.display(),
bare = bare,
"initializing repository"
);
let mut args = vec!["init".to_string()]; let mut args = vec!["init".to_string()];
if bare { if bare {
args.push("--bare".into()); args.push("--bare".into());
+12 -3
View File
@@ -10,17 +10,25 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
dotenvy::dotenv().ok(); dotenvy::dotenv().ok();
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
tracing::info!(
version = env!("CARGO_PKG_VERSION"),
"gitks starting up"
);
let host = std::env::var("GITKS_HOST").unwrap_or_else(|_| DEFAULT_HOST.into()); let host = std::env::var("GITKS_HOST").unwrap_or_else(|_| DEFAULT_HOST.into());
let port = std::env::var("GITKS_PORT").unwrap_or_else(|_| DEFAULT_PORT.into()); let port = std::env::var("GITKS_PORT").unwrap_or_else(|_| DEFAULT_PORT.into());
let repo_prefix = std::env::var("REPO_PREFIX_PATH").map_err(|_| { let repo_prefix = std::env::var("REPO_PREFIX_PATH")
"REPO_PREFIX_PATH environment variable is required (e.g. /data/repos)" .map_err(|_| "REPO_PREFIX_PATH environment variable is required (e.g. /data/repos)")?;
})?;
let repo_prefix = PathBuf::from(&repo_prefix); let repo_prefix = PathBuf::from(&repo_prefix);
if !repo_prefix.is_absolute() { if !repo_prefix.is_absolute() {
return Err("REPO_PREFIX_PATH must be an absolute path".into()); return Err("REPO_PREFIX_PATH must be an absolute path".into());
} }
if !repo_prefix.exists() { if !repo_prefix.exists() {
tracing::info!(
path = %repo_prefix.display(),
"creating repo prefix directory"
);
std::fs::create_dir_all(&repo_prefix)?; std::fs::create_dir_all(&repo_prefix)?;
} }
@@ -33,5 +41,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
serve(addr, repo_prefix).await?; serve(addr, repo_prefix).await?;
tracing::info!("gitks shut down");
Ok(()) Ok(())
} }
+8 -3
View File
@@ -6,11 +6,16 @@ impl GitBare {
pub fn merge(&self, request: MergeRequest) -> GitResult<MergeResult> { pub fn merge(&self, request: MergeRequest) -> GitResult<MergeResult> {
let target_branch = request.target_branch.clone(); let target_branch = request.target_branch.clone();
let source_revision = match request.source.and_then(|s| s.selector) { let source_revision = match request.source.and_then(|s| s.selector) {
Some(crate::pb::object_selector::Selector::Oid(oid)) => oid.hex, Some(crate::pb::object_selector::Selector::Oid(oid)) => oid.hex.clone(),
Some(crate::pb::object_selector::Selector::Revision(name)) => name.revision, Some(crate::pb::object_selector::Selector::Revision(name)) => name.revision.clone(),
None => return Err(GitError::InvalidArgument("source is required".into())), None => return Err(GitError::InvalidArgument("source is required".into())),
}; };
tracing::info!(
repo = %self.bare_dir.display(),
target = %target_branch,
source = %source_revision,
"merging"
);
let repo = self.gix_repo()?; let repo = self.gix_repo()?;
let branch_ref = format!("refs/heads/{}", target_branch); let branch_ref = format!("refs/heads/{}", target_branch);
+22 -19
View File
@@ -8,15 +8,27 @@ impl GitBare {
/// Index a pack file from streamed input. /// Index a pack file from streamed input.
/// ///
/// Client-streaming → unary response. /// Client-streaming → unary response.
/// Collects all input chunks into a single pack, then runs `git index-pack`. /// Writes each chunk directly to a temp file to avoid buffering
/// the entire pack in memory.
pub fn index_pack(&self, inputs: Vec<IndexPackRequest>) -> GitResult<IndexPackResponse> { pub fn index_pack(&self, inputs: Vec<IndexPackRequest>) -> GitResult<IndexPackResponse> {
// Reassemble all chunks into a single pack data buffer
let mut pack_data = Vec::new();
let mut strict = false; let mut strict = false;
let mut keep = false; let mut keep = false;
let mut has_data = false;
let pack_dir = self.bare_dir.join("objects").join("pack");
std::fs::create_dir_all(&pack_dir).map_err(GitError::Io)?;
// Stream pack data to a temp file instead of accumulating in memory
let mut tmp_file = tempfile::Builder::new()
.prefix("tmp_index_pack_")
.tempfile_in(&pack_dir)
.map_err(GitError::Io)?;
for input in &inputs { for input in &inputs {
pack_data.extend_from_slice(&input.data); if !input.data.is_empty() {
tmp_file.write_all(&input.data).map_err(GitError::Io)?;
has_data = true;
}
if input.strict { if input.strict {
strict = true; strict = true;
} }
@@ -25,25 +37,18 @@ impl GitBare {
} }
} }
if pack_data.is_empty() { if !has_data {
return Err(GitError::InvalidArgument("empty pack data".into())); return Err(GitError::InvalidArgument("empty pack data".into()));
} }
let pack_dir = self.bare_dir.join("objects").join("pack"); // Flush and get the path before we pass it to git
std::fs::create_dir_all(&pack_dir).map_err(GitError::Io)?; tmp_file.flush().map_err(GitError::Io)?;
// Write pack data to a unique temp file in the pack directory.
let mut tmp_file = tempfile::Builder::new()
.prefix("tmp_index_pack_")
.tempfile_in(&pack_dir)
.map_err(GitError::Io)?;
tmp_file.write_all(&pack_data).map_err(GitError::Io)?;
let tmp_path = tmp_file.path().to_path_buf(); let tmp_path = tmp_file.path().to_path_buf();
let mut args = vec![ let mut args = vec![
"--git-dir".to_string(), "--git-dir".to_string(),
self.bare_dir.to_string_lossy().into_owned(), self.bare_dir.to_string_lossy().into_owned(),
"index-pack".to_string(), "index-pack".into(),
]; ];
if strict { if strict {
args.push("--strict".into()); args.push("--strict".into());
@@ -59,6 +64,7 @@ impl GitBare {
.unchecked() .unchecked()
.run()?; .run()?;
// Drop the temp file handle — git index-pack has processed it
drop(tmp_file); drop(tmp_file);
if !result.status.success() { if !result.status.success() {
@@ -73,12 +79,9 @@ impl GitBare {
let stderr = String::from_utf8_lossy(&result.stderr); let stderr = String::from_utf8_lossy(&result.stderr);
let all_output = format!("{output}\n{stderr}"); let all_output = format!("{output}\n{stderr}");
// git index-pack outputs the .idx and .pack filenames
// e.g. "... pack-<hex>.pack ... pack-<hex>.idx"
let pack_hash = all_output let pack_hash = all_output
.lines() .lines()
.filter_map(|line| { .filter_map(|line| {
// Look for the hash after "pack-" and before ".idx" or ".pack"
let trimmed = line.trim(); let trimmed = line.trim();
if let Some(idx) = trimmed.find("pack-") { if let Some(idx) = trimmed.find("pack-") {
let rest = &trimmed[idx + 5..]; let rest = &trimmed[idx + 5..];
@@ -96,7 +99,7 @@ impl GitBare {
// Try to get object count from .idx if it exists // Try to get object count from .idx if it exists
let mut object_count = 0u64; let mut object_count = 0u64;
if let Some(ref hash) = pack_hash { if let Some(ref hash) = pack_hash {
let idx_path = pack_dir.join(format!("pack-{}.idx", hash)); let idx_path = pack_dir.join(format!("pack-{hash}.idx"));
if idx_path.exists() { if idx_path.exists() {
let verify = duct::cmd( let verify = duct::cmd(
"git", "git",
+4
View File
@@ -18,6 +18,10 @@ impl GitBare {
) -> Result<ReceiverStream<Result<PackfileChunk, tonic::Status>>, tonic::Status> { ) -> Result<ReceiverStream<Result<PackfileChunk, tonic::Status>>, tonic::Status> {
let bare_dir = self.bare_dir.clone(); let bare_dir = self.bare_dir.clone();
let bare_dir_str = bare_dir.to_string_lossy().into_owned(); let bare_dir_str = bare_dir.to_string_lossy().into_owned();
tracing::info!(
repo = %bare_dir_str,
"spawning git pack-objects subprocess"
);
let (tx, rx) = tokio::sync::mpsc::channel(8); let (tx, rx) = tokio::sync::mpsc::channel(8);
+4
View File
@@ -21,6 +21,10 @@ impl GitBare {
+ 'static, + 'static,
) -> Result<ReceiverStream<Result<ReceivePackResponse, tonic::Status>>, tonic::Status> { ) -> Result<ReceiverStream<Result<ReceivePackResponse, tonic::Status>>, tonic::Status> {
let bare_dir = self.bare_dir.to_string_lossy().into_owned(); let bare_dir = self.bare_dir.to_string_lossy().into_owned();
tracing::info!(
repo = %bare_dir,
"spawning git receive-pack subprocess"
);
let (tx, rx) = tokio::sync::mpsc::channel(16); let (tx, rx) = tokio::sync::mpsc::channel(16);
+4
View File
@@ -21,6 +21,10 @@ impl GitBare {
+ 'static, + 'static,
) -> Result<ReceiverStream<Result<UploadPackResponse, tonic::Status>>, tonic::Status> { ) -> Result<ReceiverStream<Result<UploadPackResponse, tonic::Status>>, tonic::Status> {
let bare_dir = self.bare_dir.to_string_lossy().into_owned(); let bare_dir = self.bare_dir.to_string_lossy().into_owned();
tracing::info!(
repo = %bare_dir,
"spawning git upload-pack subprocess"
);
let (tx, rx) = tokio::sync::mpsc::channel(16); let (tx, rx) = tokio::sync::mpsc::channel(16);
+18 -4
View File
@@ -1,6 +1,6 @@
use crate::pb::*; use crate::pb::*;
use super::{GitksService, into_status, into_stream}; use super::{GitksService, cache, into_status};
#[tonic::async_trait] #[tonic::async_trait]
impl archive_service_server::ArchiveService for GitksService { impl archive_service_server::ArchiveService for GitksService {
@@ -12,9 +12,13 @@ impl archive_service_server::ArchiveService for GitksService {
request: tonic::Request<ArchiveRequest>, request: tonic::Request<ArchiveRequest>,
) -> Result<tonic::Response<Self::GetArchiveStream>, tonic::Status> { ) -> Result<tonic::Response<Self::GetArchiveStream>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("archive.get_archive", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let chunks = gb.get_archive(inner).map_err(into_status)?; let stream = gb.get_archive_stream(inner)?;
Ok(tonic::Response::new(into_stream(chunks))) tracing::info!(%repo, "archive streaming started");
Ok(tonic::Response::new(stream))
} }
async fn list_archive_entries( async fn list_archive_entries(
@@ -22,8 +26,18 @@ impl archive_service_server::ArchiveService for GitksService {
request: tonic::Request<ListArchiveEntriesRequest>, request: tonic::Request<ListArchiveEntriesRequest>,
) -> Result<tonic::Response<ListArchiveEntriesResponse>, tonic::Status> { ) -> Result<tonic::Response<ListArchiveEntriesResponse>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("archive.list_archive_entries", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.list_archive_entries(inner).map_err(into_status)?; let resp = if cache::selector_is_oid(&inner.treeish) {
cache::cached_response("archive.list_archive_entries", &inner, || {
gb.list_archive_entries(inner.clone()).map_err(into_status)
})?
} else {
gb.list_archive_entries(inner).map_err(into_status)?
};
tracing::info!(%repo, count = resp.entries.len(), "list_archive_entries done");
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
} }
+24 -3
View File
@@ -1,6 +1,6 @@
use crate::pb::*; use crate::pb::*;
use super::{GitksService, into_status, into_stream}; use super::{GitksService, cache, into_status, into_stream};
#[tonic::async_trait] #[tonic::async_trait]
impl blame_service_server::BlameService for GitksService { impl blame_service_server::BlameService for GitksService {
@@ -12,8 +12,19 @@ impl blame_service_server::BlameService for GitksService {
request: tonic::Request<BlameRequest>, request: tonic::Request<BlameRequest>,
) -> Result<tonic::Response<BlameResponse>, tonic::Status> { ) -> Result<tonic::Response<BlameResponse>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let path = inner.path.clone();
let span = tracing::info_span!("blame.blame", %repo, %path);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.blame(inner).map_err(into_status)?; let resp = if cache::selector_is_oid(&inner.revision) {
cache::cached_response("blame.blame", &inner, || {
gb.blame(inner.clone()).map_err(into_status)
})?
} else {
gb.blame(inner).map_err(into_status)?
};
tracing::info!(%repo, %path, hunks = resp.hunks.len(), "blame done");
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
@@ -22,8 +33,18 @@ impl blame_service_server::BlameService for GitksService {
request: tonic::Request<BlameRequest>, request: tonic::Request<BlameRequest>,
) -> Result<tonic::Response<Self::StreamBlameStream>, tonic::Status> { ) -> Result<tonic::Response<Self::StreamBlameStream>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let path = inner.path.clone();
let span = tracing::info_span!("blame.stream_blame", %repo, %path);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.blame(inner).map_err(into_status)?; let resp = if cache::selector_is_oid(&inner.revision) {
cache::cached_response("blame.blame", &inner, || {
gb.blame(inner.clone()).map_err(into_status)
})?
} else {
gb.blame(inner).map_err(into_status)?
};
Ok(tonic::Response::new(into_stream(resp.hunks))) Ok(tonic::Response::new(into_stream(resp.hunks)))
} }
} }
+40
View File
@@ -9,8 +9,12 @@ impl branch_service_server::BranchService for GitksService {
request: tonic::Request<ListBranchesRequest>, request: tonic::Request<ListBranchesRequest>,
) -> Result<tonic::Response<ListBranchesResponse>, tonic::Status> { ) -> Result<tonic::Response<ListBranchesResponse>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("branch.list_branches", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.list_branches(inner).map_err(into_status)?; let resp = gb.list_branches(inner).map_err(into_status)?;
tracing::info!(%repo, count = resp.branches.len(), "list_branches done");
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
@@ -19,6 +23,10 @@ impl branch_service_server::BranchService for GitksService {
request: tonic::Request<GetBranchRequest>, request: tonic::Request<GetBranchRequest>,
) -> Result<tonic::Response<Branch>, tonic::Status> { ) -> Result<tonic::Response<Branch>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let name = inner.name.clone();
let span = tracing::info_span!("branch.get_branch", %repo, %name);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.get_branch(inner).map_err(into_status)?; let resp = gb.get_branch(inner).map_err(into_status)?;
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
@@ -29,8 +37,13 @@ impl branch_service_server::BranchService for GitksService {
request: tonic::Request<CreateBranchRequest>, request: tonic::Request<CreateBranchRequest>,
) -> Result<tonic::Response<Branch>, tonic::Status> { ) -> Result<tonic::Response<Branch>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let name = inner.name.clone();
let span = tracing::info_span!("branch.create_branch", %repo, %name);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.create_branch(inner).map_err(into_status)?; let resp = gb.create_branch(inner).map_err(into_status)?;
tracing::info!(%repo, %name, "branch created");
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
@@ -39,8 +52,13 @@ impl branch_service_server::BranchService for GitksService {
request: tonic::Request<DeleteBranchRequest>, request: tonic::Request<DeleteBranchRequest>,
) -> Result<tonic::Response<()>, tonic::Status> { ) -> Result<tonic::Response<()>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let name = inner.name.clone();
let span = tracing::info_span!("branch.delete_branch", %repo, %name);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
gb.delete_branch(inner).map_err(into_status)?; gb.delete_branch(inner).map_err(into_status)?;
tracing::info!(%repo, %name, "branch deleted");
Ok(tonic::Response::new(())) Ok(tonic::Response::new(()))
} }
@@ -49,8 +67,14 @@ impl branch_service_server::BranchService for GitksService {
request: tonic::Request<RenameBranchRequest>, request: tonic::Request<RenameBranchRequest>,
) -> Result<tonic::Response<Branch>, tonic::Status> { ) -> Result<tonic::Response<Branch>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let old = inner.old_name.clone();
let new = inner.new_name.clone();
let span = tracing::info_span!("branch.rename_branch", %repo, %old, %new);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.rename_branch(inner).map_err(into_status)?; let resp = gb.rename_branch(inner).map_err(into_status)?;
tracing::info!(%repo, old = %old, new = %new, "branch renamed");
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
@@ -59,8 +83,13 @@ impl branch_service_server::BranchService for GitksService {
request: tonic::Request<UpdateBranchTargetRequest>, request: tonic::Request<UpdateBranchTargetRequest>,
) -> Result<tonic::Response<Branch>, tonic::Status> { ) -> Result<tonic::Response<Branch>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let name = inner.name.clone();
let span = tracing::info_span!("branch.update_branch_target", %repo, %name);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.update_branch_target(inner).map_err(into_status)?; let resp = gb.update_branch_target(inner).map_err(into_status)?;
tracing::info!(%repo, %name, "branch target updated");
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
@@ -69,8 +98,13 @@ impl branch_service_server::BranchService for GitksService {
request: tonic::Request<SetBranchUpstreamRequest>, request: tonic::Request<SetBranchUpstreamRequest>,
) -> Result<tonic::Response<Branch>, tonic::Status> { ) -> Result<tonic::Response<Branch>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let name = inner.name.clone();
let span = tracing::info_span!("branch.set_branch_upstream", %repo, %name);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.set_branch_upstream(inner).map_err(into_status)?; let resp = gb.set_branch_upstream(inner).map_err(into_status)?;
tracing::info!(%repo, %name, "branch upstream set");
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
@@ -79,8 +113,14 @@ impl branch_service_server::BranchService for GitksService {
request: tonic::Request<CompareBranchRequest>, request: tonic::Request<CompareBranchRequest>,
) -> Result<tonic::Response<CompareBranchResponse>, tonic::Status> { ) -> Result<tonic::Response<CompareBranchResponse>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let source = inner.source_branch.clone();
let target = inner.target_branch.clone();
let span = tracing::info_span!("branch.compare_branch", %repo, %source, %target);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.compare_branch(inner).map_err(into_status)?; let resp = gb.compare_branch(inner).map_err(into_status)?;
tracing::info!(%repo, %source, %target, ahead = resp.ahead_by, behind = resp.behind_by, "branch compared");
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
} }
+62 -5
View File
@@ -1,6 +1,6 @@
use crate::pb::*; use crate::pb::*;
use super::{GitksService, into_status}; use super::{GitksService, cache, into_status};
#[tonic::async_trait] #[tonic::async_trait]
impl commit_service_server::CommitService for GitksService { impl commit_service_server::CommitService for GitksService {
@@ -9,8 +9,18 @@ impl commit_service_server::CommitService for GitksService {
request: tonic::Request<ListCommitsRequest>, request: tonic::Request<ListCommitsRequest>,
) -> Result<tonic::Response<ListCommitsResponse>, tonic::Status> { ) -> Result<tonic::Response<ListCommitsResponse>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("commit.list_commits", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.list_commits(inner).map_err(into_status)?; let resp = if !inner.all && cache::selector_is_oid(&inner.revision) {
cache::cached_response("commit.list_commits", &inner, || {
gb.list_commits(inner.clone()).map_err(into_status)
})?
} else {
gb.list_commits(inner).map_err(into_status)?
};
tracing::info!(%repo, count = resp.commits.len(), total = resp.page_info.as_ref().map(|p| p.total_count).unwrap_or(0), "list_commits done");
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
@@ -19,8 +29,17 @@ impl commit_service_server::CommitService for GitksService {
request: tonic::Request<GetCommitRequest>, request: tonic::Request<GetCommitRequest>,
) -> Result<tonic::Response<Commit>, tonic::Status> { ) -> Result<tonic::Response<Commit>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("commit.get_commit", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.get_commit(inner).map_err(into_status)?; let resp = if cache::selector_is_oid(&inner.revision) {
cache::cached_response("commit.get_commit", &inner, || {
gb.get_commit(inner.clone()).map_err(into_status)
})?
} else {
gb.get_commit(inner).map_err(into_status)?
};
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
@@ -29,8 +48,18 @@ impl commit_service_server::CommitService for GitksService {
request: tonic::Request<GetCommitAncestorsRequest>, request: tonic::Request<GetCommitAncestorsRequest>,
) -> Result<tonic::Response<GetCommitAncestorsResponse>, tonic::Status> { ) -> Result<tonic::Response<GetCommitAncestorsResponse>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("commit.get_commit_ancestors", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.get_commit_ancestors(inner).map_err(into_status)?; let resp = if cache::selector_is_oid(&inner.revision) {
cache::cached_response("commit.get_commit_ancestors", &inner, || {
gb.get_commit_ancestors(inner.clone()).map_err(into_status)
})?
} else {
gb.get_commit_ancestors(inner).map_err(into_status)?
};
tracing::info!(%repo, count = resp.commits.len(), "get_commit_ancestors done");
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
@@ -39,8 +68,16 @@ impl commit_service_server::CommitService for GitksService {
request: tonic::Request<CreateCommitRequest>, request: tonic::Request<CreateCommitRequest>,
) -> Result<tonic::Response<CreateCommitResponse>, tonic::Status> { ) -> Result<tonic::Response<CreateCommitResponse>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let branch = inner.branch.clone();
let span = tracing::info_span!("commit.create_commit", %repo, %branch);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.create_commit(inner).map_err(into_status)?; let resp = gb.create_commit(inner).map_err(into_status)?;
let commit_hex = resp.commit.as_ref()
.and_then(|c| c.oid.as_ref().map(|o| o.hex.as_str()).or(Some("?")))
.unwrap_or("?");
tracing::info!(%repo, %branch, %commit_hex, "commit created");
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
@@ -49,8 +86,13 @@ impl commit_service_server::CommitService for GitksService {
request: tonic::Request<RevertCommitRequest>, request: tonic::Request<RevertCommitRequest>,
) -> Result<tonic::Response<CreateCommitResponse>, tonic::Status> { ) -> Result<tonic::Response<CreateCommitResponse>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let branch = inner.branch.clone();
let span = tracing::info_span!("commit.revert_commit", %repo, %branch);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.revert_commit(inner).map_err(into_status)?; let resp = gb.revert_commit(inner).map_err(into_status)?;
tracing::info!(%repo, %branch, "commit reverted");
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
@@ -59,8 +101,13 @@ impl commit_service_server::CommitService for GitksService {
request: tonic::Request<CherryPickCommitRequest>, request: tonic::Request<CherryPickCommitRequest>,
) -> Result<tonic::Response<CreateCommitResponse>, tonic::Status> { ) -> Result<tonic::Response<CreateCommitResponse>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let branch = inner.branch.clone();
let span = tracing::info_span!("commit.cherry_pick_commit", %repo, %branch);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.cherry_pick_commit(inner).map_err(into_status)?; let resp = gb.cherry_pick_commit(inner).map_err(into_status)?;
tracing::info!(%repo, %branch, "commit cherry-picked");
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
@@ -69,8 +116,18 @@ impl commit_service_server::CommitService for GitksService {
request: tonic::Request<CompareCommitsRequest>, request: tonic::Request<CompareCommitsRequest>,
) -> Result<tonic::Response<CompareCommitsResponse>, tonic::Status> { ) -> Result<tonic::Response<CompareCommitsResponse>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("commit.compare_commits", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.compare_commits(inner).map_err(into_status)?; let resp = if cache::selectors_are_oid(&inner.base, &inner.head) {
cache::cached_response("commit.compare_commits", &inner, || {
gb.compare_commits(inner.clone()).map_err(into_status)
})?
} else {
gb.compare_commits(inner).map_err(into_status)?
};
tracing::info!(%repo, count = resp.commits.len(), "compare_commits done");
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
} }
+43 -5
View File
@@ -1,6 +1,6 @@
use crate::pb::*; use crate::pb::*;
use super::{GitksService, into_status, into_stream}; use super::{GitksService, cache, into_status, into_stream};
#[tonic::async_trait] #[tonic::async_trait]
impl diff_service_server::DiffService for GitksService { impl diff_service_server::DiffService for GitksService {
@@ -12,8 +12,18 @@ impl diff_service_server::DiffService for GitksService {
request: tonic::Request<GetDiffRequest>, request: tonic::Request<GetDiffRequest>,
) -> Result<tonic::Response<GetDiffResponse>, tonic::Status> { ) -> Result<tonic::Response<GetDiffResponse>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("diff.get_diff", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.get_diff(inner).map_err(into_status)?; let resp = if cache::selectors_are_oid(&inner.base, &inner.head) {
cache::cached_response("diff.get_diff", &inner, || {
gb.get_diff(inner.clone()).map_err(into_status)
})?
} else {
gb.get_diff(inner).map_err(into_status)?
};
tracing::info!(%repo, files = resp.files.len(), overflow = resp.overflow, "get_diff done");
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
@@ -22,8 +32,18 @@ impl diff_service_server::DiffService for GitksService {
request: tonic::Request<GetCommitDiffRequest>, request: tonic::Request<GetCommitDiffRequest>,
) -> Result<tonic::Response<GetDiffResponse>, tonic::Status> { ) -> Result<tonic::Response<GetDiffResponse>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("diff.get_commit_diff", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.get_commit_diff(inner).map_err(into_status)?; let resp = if cache::selector_is_oid(&inner.commit) {
cache::cached_response("diff.get_commit_diff", &inner, || {
gb.get_commit_diff(inner.clone()).map_err(into_status)
})?
} else {
gb.get_commit_diff(inner).map_err(into_status)?
};
tracing::info!(%repo, files = resp.files.len(), "get_commit_diff done");
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
@@ -32,8 +52,17 @@ impl diff_service_server::DiffService for GitksService {
request: tonic::Request<GetPatchRequest>, request: tonic::Request<GetPatchRequest>,
) -> Result<tonic::Response<Self::GetPatchStream>, tonic::Status> { ) -> Result<tonic::Response<Self::GetPatchStream>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("diff.get_patch", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let items = gb.get_patch(inner).map_err(into_status)?; let items = if cache::selectors_are_oid(&inner.base, &inner.head) {
cache::cached_vec_response("diff.get_patch", &inner, || {
gb.get_patch(inner.clone()).map_err(into_status)
})?
} else {
gb.get_patch(inner).map_err(into_status)?
};
Ok(tonic::Response::new(into_stream(items))) Ok(tonic::Response::new(into_stream(items)))
} }
@@ -42,8 +71,17 @@ impl diff_service_server::DiffService for GitksService {
request: tonic::Request<GetDiffStatsRequest>, request: tonic::Request<GetDiffStatsRequest>,
) -> Result<tonic::Response<DiffStats>, tonic::Status> { ) -> Result<tonic::Response<DiffStats>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("diff.get_diff_stats", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.get_diff_stats(inner).map_err(into_status)?; let resp = if cache::selectors_are_oid(&inner.base, &inner.head) {
cache::cached_response("diff.get_diff_stats", &inner, || {
gb.get_diff_stats(inner.clone()).map_err(into_status)
})?
} else {
gb.get_diff_stats(inner).map_err(into_status)?
};
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
} }
+23
View File
@@ -9,8 +9,12 @@ impl merge_service_server::MergeService for GitksService {
request: tonic::Request<CheckMergeRequest>, request: tonic::Request<CheckMergeRequest>,
) -> Result<tonic::Response<MergeResult>, tonic::Status> { ) -> Result<tonic::Response<MergeResult>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("merge.check_merge", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.check_merge(inner).map_err(into_status)?; let resp = gb.check_merge(inner).map_err(into_status)?;
tracing::info!(%repo, status = resp.status, "check_merge done");
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
@@ -19,8 +23,13 @@ impl merge_service_server::MergeService for GitksService {
request: tonic::Request<MergeRequest>, request: tonic::Request<MergeRequest>,
) -> Result<tonic::Response<MergeResult>, tonic::Status> { ) -> Result<tonic::Response<MergeResult>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let target = inner.target_branch.clone();
let span = tracing::info_span!("merge.merge", %repo, %target);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.merge(inner).map_err(into_status)?; let resp = gb.merge(inner).map_err(into_status)?;
tracing::info!(%repo, %target, status = resp.status, "merge done");
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
@@ -29,8 +38,12 @@ impl merge_service_server::MergeService for GitksService {
request: tonic::Request<ListMergeConflictsRequest>, request: tonic::Request<ListMergeConflictsRequest>,
) -> Result<tonic::Response<ListMergeConflictsResponse>, tonic::Status> { ) -> Result<tonic::Response<ListMergeConflictsResponse>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("merge.list_merge_conflicts", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.list_merge_conflicts(inner).map_err(into_status)?; let resp = gb.list_merge_conflicts(inner).map_err(into_status)?;
tracing::info!(%repo, conflicts = resp.conflicts.len(), "list_merge_conflicts done");
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
@@ -39,8 +52,13 @@ impl merge_service_server::MergeService for GitksService {
request: tonic::Request<ResolveMergeConflictsRequest>, request: tonic::Request<ResolveMergeConflictsRequest>,
) -> Result<tonic::Response<MergeResult>, tonic::Status> { ) -> Result<tonic::Response<MergeResult>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let target = inner.target_branch.clone();
let span = tracing::info_span!("merge.resolve_merge_conflicts", %repo, %target);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.resolve_merge_conflicts(inner).map_err(into_status)?; let resp = gb.resolve_merge_conflicts(inner).map_err(into_status)?;
tracing::info!(%repo, %target, status = resp.status, "merge conflicts resolved");
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
@@ -49,8 +67,13 @@ impl merge_service_server::MergeService for GitksService {
request: tonic::Request<RebaseRequest>, request: tonic::Request<RebaseRequest>,
) -> Result<tonic::Response<RebaseResult>, tonic::Status> { ) -> Result<tonic::Response<RebaseResult>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let branch = inner.branch.clone();
let span = tracing::info_span!("merge.rebase", %repo, %branch);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.rebase(inner).map_err(into_status)?; let resp = gb.rebase(inner).map_err(into_status)?;
tracing::info!(%repo, %branch, status = resp.status, "rebase done");
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
} }
+61 -17
View File
@@ -1,6 +1,7 @@
mod archive; mod archive;
mod blame; mod blame;
mod branch; mod branch;
mod cache;
mod commit; mod commit;
mod diff; mod diff;
mod merge; mod merge;
@@ -23,11 +24,23 @@ use crate::pb::{
#[derive(Clone)] #[derive(Clone)]
pub struct GitksService { pub struct GitksService {
/// 所有仓库的根路径前缀 /// Root prefix path for all repositories
pub(crate) repo_prefix: PathBuf, pub repo_prefix: PathBuf,
} }
impl GitksService { 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( pub(crate) fn resolve(
&self, &self,
header: Option<&crate::pb::RepositoryHeader>, header: Option<&crate::pb::RepositoryHeader>,
@@ -35,7 +48,12 @@ impl GitksService {
let header = let header =
header.ok_or_else(|| tonic::Status::invalid_argument("repository is required"))?; header.ok_or_else(|| tonic::Status::invalid_argument("repository is required"))?;
let header = self.prefixed_header(header); let header = self.prefixed_header(header);
GitBare::from_repository_header(&header).map_err(into_status) 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( pub(crate) fn resolve_for_init(
@@ -49,7 +67,7 @@ impl GitksService {
return Err(tonic::Status::invalid_argument("relative_path is required")); return Err(tonic::Status::invalid_argument("relative_path is required"));
} }
let candidate = self.repo_prefix.join(relative_path); let candidate = self.repo_prefix.join(relative_path);
// 路径穿越检查 // Path traversal check
let canonical = candidate let canonical = candidate
.canonicalize() .canonicalize()
.unwrap_or_else(|_| candidate.clone()); .unwrap_or_else(|_| candidate.clone());
@@ -65,11 +83,8 @@ impl GitksService {
Ok(canonical) Ok(canonical)
} }
/// 将客户端传入的 header 注入 repo_prefix 作为 storage_path /// Inject repo_prefix as storage_path into the client-provided header
fn prefixed_header( fn prefixed_header(&self, header: &crate::pb::RepositoryHeader) -> crate::pb::RepositoryHeader {
&self,
header: &crate::pb::RepositoryHeader,
) -> crate::pb::RepositoryHeader {
crate::pb::RepositoryHeader { crate::pb::RepositoryHeader {
storage_path: self.repo_prefix.to_string_lossy().into_owned(), storage_path: self.repo_prefix.to_string_lossy().into_owned(),
relative_path: header.relative_path.clone(), relative_path: header.relative_path.clone(),
@@ -115,20 +130,49 @@ pub(crate) fn git_cmd(gb: &GitBare, args: &[&str]) -> Result<std::process::Outpu
gb.bare_dir.to_string_lossy().into_owned(), gb.bare_dir.to_string_lossy().into_owned(),
]; ];
full_args.extend(args.iter().map(|s| s.to_string())); full_args.extend(args.iter().map(|s| s.to_string()));
std::process::Command::new("git") 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) .args(&full_args)
.output() .output()
.map_err(|e| tonic::Status::internal(e.to_string())) .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( pub async fn serve(
addr: std::net::SocketAddr, addr: std::net::SocketAddr,
repo_prefix: PathBuf, repo_prefix: PathBuf,
) -> Result<(), tonic::transport::Error> { ) -> Result<(), tonic::transport::Error> {
let span = tracing::info_span!("gitks.server", %addr);
let _enter = span.enter();
let svc = GitksService { repo_prefix }; let svc = GitksService { repo_prefix };
tonic::transport::Server::builder() tracing::info!("registering gRPC services");
.add_service(repository_service_server::RepositoryServiceServer::new(svc.clone())) let server = tonic::transport::Server::builder()
.add_service(archive_service_server::ArchiveServiceServer::new(svc.clone())) .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(blame_service_server::BlameServiceServer::new(svc.clone()))
.add_service(branch_service_server::BranchServiceServer::new(svc.clone())) .add_service(branch_service_server::BranchServiceServer::new(svc.clone()))
.add_service(commit_service_server::CommitServiceServer::new(svc.clone())) .add_service(commit_service_server::CommitServiceServer::new(svc.clone()))
@@ -136,7 +180,7 @@ pub async fn serve(
.add_service(merge_service_server::MergeServiceServer::new(svc.clone())) .add_service(merge_service_server::MergeServiceServer::new(svc.clone()))
.add_service(pack_service_server::PackServiceServer::new(svc.clone())) .add_service(pack_service_server::PackServiceServer::new(svc.clone()))
.add_service(tag_service_server::TagServiceServer::new(svc.clone())) .add_service(tag_service_server::TagServiceServer::new(svc.clone()))
.add_service(tree_service_server::TreeServiceServer::new(svc)) .add_service(tree_service_server::TreeServiceServer::new(svc));
.serve(addr) tracing::info!("server ready, starting to accept connections");
.await server.serve(addr).await
} }
+28
View File
@@ -16,8 +16,12 @@ impl pack_service_server::PackService for GitksService {
request: tonic::Request<AdvertiseRefsRequest>, request: tonic::Request<AdvertiseRefsRequest>,
) -> Result<tonic::Response<AdvertiseRefsResponse>, tonic::Status> { ) -> Result<tonic::Response<AdvertiseRefsResponse>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("pack.advertise_refs", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.advertise_refs(inner).map_err(into_status)?; let resp = gb.advertise_refs(inner).map_err(into_status)?;
tracing::info!(%repo, refs = resp.references.len(), "advertise_refs done");
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
@@ -30,6 +34,10 @@ impl pack_service_server::PackService for GitksService {
.next() .next()
.await .await
.ok_or_else(|| tonic::Status::invalid_argument("empty upload-pack stream"))??; .ok_or_else(|| tonic::Status::invalid_argument("empty upload-pack stream"))??;
let repo = self.repo_label(first.repository.as_ref());
let span = tracing::info_span!("pack.upload_pack", %repo);
let _enter = span.enter();
tracing::info!(%repo, "upload-pack streaming started");
let gb = self.resolve(first.repository.as_ref())?; let gb = self.resolve(first.repository.as_ref())?;
let (tx, rx) = tokio::sync::mpsc::channel(16); let (tx, rx) = tokio::sync::mpsc::channel(16);
@@ -57,6 +65,10 @@ impl pack_service_server::PackService for GitksService {
.next() .next()
.await .await
.ok_or_else(|| tonic::Status::invalid_argument("empty receive-pack stream"))??; .ok_or_else(|| tonic::Status::invalid_argument("empty receive-pack stream"))??;
let repo = self.repo_label(first.repository.as_ref());
let span = tracing::info_span!("pack.receive_pack", %repo);
let _enter = span.enter();
tracing::info!(%repo, "receive-pack streaming started");
let gb = self.resolve(first.repository.as_ref())?; let gb = self.resolve(first.repository.as_ref())?;
let (tx, rx) = tokio::sync::mpsc::channel(16); let (tx, rx) = tokio::sync::mpsc::channel(16);
@@ -80,8 +92,12 @@ impl pack_service_server::PackService for GitksService {
request: tonic::Request<PackObjectsRequest>, request: tonic::Request<PackObjectsRequest>,
) -> Result<tonic::Response<Self::PackObjectsStream>, tonic::Status> { ) -> Result<tonic::Response<Self::PackObjectsStream>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("pack.pack_objects", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let stream = gb.pack_objects(inner).await?; let stream = gb.pack_objects(inner).await?;
tracing::info!(%repo, "pack-objects streaming started");
Ok(tonic::Response::new(stream)) Ok(tonic::Response::new(stream))
} }
@@ -94,8 +110,12 @@ impl pack_service_server::PackService for GitksService {
while let Some(msg) = stream.next().await { while let Some(msg) = stream.next().await {
inputs.push(msg?); inputs.push(msg?);
} }
let repo = self.repo_label(inputs.first().and_then(|r| r.repository.as_ref()));
let span = tracing::info_span!("pack.index_pack", %repo);
let _enter = span.enter();
let gb = self.resolve(inputs.first().and_then(|r| r.repository.as_ref()))?; let gb = self.resolve(inputs.first().and_then(|r| r.repository.as_ref()))?;
let resp = gb.index_pack(inputs).map_err(into_status)?; let resp = gb.index_pack(inputs).map_err(into_status)?;
tracing::info!(%repo, objects = resp.object_count, "index_pack done");
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
@@ -104,8 +124,12 @@ impl pack_service_server::PackService for GitksService {
request: tonic::Request<ListPackfilesRequest>, request: tonic::Request<ListPackfilesRequest>,
) -> Result<tonic::Response<ListPackfilesResponse>, tonic::Status> { ) -> Result<tonic::Response<ListPackfilesResponse>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("pack.list_packfiles", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.list_packfiles(inner).map_err(into_status)?; let resp = gb.list_packfiles(inner).map_err(into_status)?;
tracing::info!(%repo, count = resp.packfiles.len(), "list_packfiles done");
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
@@ -114,8 +138,12 @@ impl pack_service_server::PackService for GitksService {
request: tonic::Request<FsckRequest>, request: tonic::Request<FsckRequest>,
) -> Result<tonic::Response<FsckResponse>, tonic::Status> { ) -> Result<tonic::Response<FsckResponse>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("pack.fsck", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.fsck(inner).map_err(into_status)?; let resp = gb.fsck(inner).map_err(into_status)?;
tracing::info!(%repo, ok = resp.ok, errors = resp.errors.len(), warnings = resp.warnings.len(), "fsck done");
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
} }
+61 -15
View File
@@ -21,6 +21,9 @@ impl repository_service_server::RepositoryService for GitksService {
request: tonic::Request<GetRepositoryRequest>, request: tonic::Request<GetRepositoryRequest>,
) -> Result<tonic::Response<Repository>, tonic::Status> { ) -> Result<tonic::Response<Repository>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("repo.get_repository", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let bare = gb.bare_dir.join("HEAD").exists(); let bare = gb.bare_dir.join("HEAD").exists();
let object_format = gb.object_format(); let object_format = gb.object_format();
@@ -38,9 +41,13 @@ impl repository_service_server::RepositoryService for GitksService {
request: tonic::Request<InitRepositoryRequest>, request: tonic::Request<InitRepositoryRequest>,
) -> Result<tonic::Response<Repository>, tonic::Status> { ) -> Result<tonic::Response<Repository>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("repo.init_repository", %repo);
let _enter = span.enter();
let bare_dir = self.resolve_for_init(inner.repository.as_ref())?; let bare_dir = self.resolve_for_init(inner.repository.as_ref())?;
let gb = crate::bare::GitBare { bare_dir }; let gb = crate::bare::GitBare::new(bare_dir);
gb.init_repository(inner.bare).map_err(into_status)?; gb.init_repository(inner.bare).map_err(into_status)?;
tracing::info!(%repo, bare = inner.bare, "repository initialized");
Ok(tonic::Response::new(Repository { Ok(tonic::Response::new(Repository {
header: inner.repository, header: inner.repository,
bare: inner.bare, bare: inner.bare,
@@ -53,8 +60,13 @@ impl repository_service_server::RepositoryService for GitksService {
request: tonic::Request<DeleteRepositoryRequest>, request: tonic::Request<DeleteRepositoryRequest>,
) -> Result<tonic::Response<()>, tonic::Status> { ) -> Result<tonic::Response<()>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("repo.delete_repository", %repo);
let _enter = span.enter();
let bare_dir = self.resolve_for_init(inner.repository.as_ref())?; let bare_dir = self.resolve_for_init(inner.repository.as_ref())?;
tracing::warn!(%repo, path = %bare_dir.display(), "deleting repository");
std::fs::remove_dir_all(&bare_dir).map_err(|e| tonic::Status::internal(e.to_string()))?; std::fs::remove_dir_all(&bare_dir).map_err(|e| tonic::Status::internal(e.to_string()))?;
tracing::info!(%repo, "repository deleted");
Ok(tonic::Response::new(())) Ok(tonic::Response::new(()))
} }
@@ -63,6 +75,9 @@ impl repository_service_server::RepositoryService for GitksService {
request: tonic::Request<RepositoryExistsRequest>, request: tonic::Request<RepositoryExistsRequest>,
) -> Result<tonic::Response<RepositoryExistsResponse>, tonic::Status> { ) -> Result<tonic::Response<RepositoryExistsResponse>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("repo.repository_exists", %repo);
let _enter = span.enter();
let bare_dir = self.resolve_for_init(inner.repository.as_ref())?; let bare_dir = self.resolve_for_init(inner.repository.as_ref())?;
let exists = bare_dir.exists() && bare_dir.is_dir() && bare_dir.join("HEAD").exists(); let exists = bare_dir.exists() && bare_dir.is_dir() && bare_dir.join("HEAD").exists();
Ok(tonic::Response::new(RepositoryExistsResponse { exists })) Ok(tonic::Response::new(RepositoryExistsResponse { exists }))
@@ -73,6 +88,9 @@ impl repository_service_server::RepositoryService for GitksService {
request: tonic::Request<RepositoryObjectFormatRequest>, request: tonic::Request<RepositoryObjectFormatRequest>,
) -> Result<tonic::Response<RepositoryObjectFormatResponse>, tonic::Status> { ) -> Result<tonic::Response<RepositoryObjectFormatResponse>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("repo.get_object_format", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
Ok(tonic::Response::new(RepositoryObjectFormatResponse { Ok(tonic::Response::new(RepositoryObjectFormatResponse {
object_format: gb.object_format() as i32, object_format: gb.object_format() as i32,
@@ -84,6 +102,9 @@ impl repository_service_server::RepositoryService for GitksService {
request: tonic::Request<GetDefaultBranchRequest>, request: tonic::Request<GetDefaultBranchRequest>,
) -> Result<tonic::Response<GetDefaultBranchResponse>, tonic::Status> { ) -> Result<tonic::Response<GetDefaultBranchResponse>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("repo.get_default_branch", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
Ok(tonic::Response::new(GetDefaultBranchResponse { Ok(tonic::Response::new(GetDefaultBranchResponse {
name: default_branch_name(&gb), name: default_branch_name(&gb),
@@ -95,6 +116,10 @@ impl repository_service_server::RepositoryService for GitksService {
request: tonic::Request<SetDefaultBranchRequest>, request: tonic::Request<SetDefaultBranchRequest>,
) -> Result<tonic::Response<()>, tonic::Status> { ) -> Result<tonic::Response<()>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let name = inner.name.clone();
let span = tracing::info_span!("repo.set_default_branch", %repo, %name);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let refname = format!("refs/heads/{}", inner.name); let refname = format!("refs/heads/{}", inner.name);
let out = git_cmd(&gb, &["symbolic-ref", "HEAD", &refname])?; let out = git_cmd(&gb, &["symbolic-ref", "HEAD", &refname])?;
@@ -103,6 +128,7 @@ impl repository_service_server::RepositoryService for GitksService {
String::from_utf8_lossy(&out.stderr).trim().to_string(), String::from_utf8_lossy(&out.stderr).trim().to_string(),
)); ));
} }
tracing::info!(%repo, %name, "default branch set");
Ok(tonic::Response::new(())) Ok(tonic::Response::new(()))
} }
@@ -111,6 +137,9 @@ impl repository_service_server::RepositoryService for GitksService {
request: tonic::Request<GetRepositoryConfigRequest>, request: tonic::Request<GetRepositoryConfigRequest>,
) -> Result<tonic::Response<GetRepositoryConfigResponse>, tonic::Status> { ) -> Result<tonic::Response<GetRepositoryConfigResponse>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("repo.get_repository_config", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let mut entries = Vec::new(); let mut entries = Vec::new();
if inner.keys.is_empty() { if inner.keys.is_empty() {
@@ -156,6 +185,9 @@ impl repository_service_server::RepositoryService for GitksService {
request: tonic::Request<SetRepositoryConfigRequest>, request: tonic::Request<SetRepositoryConfigRequest>,
) -> Result<tonic::Response<()>, tonic::Status> { ) -> Result<tonic::Response<()>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("repo.set_repository_config", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
for entry in &inner.entries { for entry in &inner.entries {
if entry.values.is_empty() { if entry.values.is_empty() {
@@ -178,6 +210,9 @@ impl repository_service_server::RepositoryService for GitksService {
request: tonic::Request<RepositoryStatisticsRequest>, request: tonic::Request<RepositoryStatisticsRequest>,
) -> Result<tonic::Response<RepositoryStatistics>, tonic::Status> { ) -> Result<tonic::Response<RepositoryStatistics>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("repo.get_repository_statistics", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
Ok(tonic::Response::new(repository_maint::get_statistics(&gb))) Ok(tonic::Response::new(repository_maint::get_statistics(&gb)))
} }
@@ -187,11 +222,13 @@ impl repository_service_server::RepositoryService for GitksService {
request: tonic::Request<RepositoryHealthRequest>, request: tonic::Request<RepositoryHealthRequest>,
) -> Result<tonic::Response<RepositoryHealthResponse>, tonic::Status> { ) -> Result<tonic::Response<RepositoryHealthResponse>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("repo.check_repository_health", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
Ok(tonic::Response::new(repository_maint::check_health( let resp = repository_maint::check_health(&gb, inner.connectivity_only)?;
&gb, tracing::info!(%repo, ok = resp.ok, errors = resp.errors.len(), warnings = resp.warnings.len(), "health check done");
inner.connectivity_only, Ok(tonic::Response::new(resp))
)?))
} }
async fn garbage_collect( async fn garbage_collect(
@@ -199,12 +236,13 @@ impl repository_service_server::RepositoryService for GitksService {
request: tonic::Request<GarbageCollectRequest>, request: tonic::Request<GarbageCollectRequest>,
) -> Result<tonic::Response<RepositoryMaintenanceResponse>, tonic::Status> { ) -> Result<tonic::Response<RepositoryMaintenanceResponse>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("repo.garbage_collect", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
Ok(tonic::Response::new(repository_maint::run_gc( let resp = repository_maint::run_gc(&gb, inner.prune, inner.aggressive)?;
&gb, tracing::info!(%repo, ok = resp.ok, "gc done");
inner.prune, Ok(tonic::Response::new(resp))
inner.aggressive,
)?))
} }
async fn repack( async fn repack(
@@ -212,13 +250,18 @@ impl repository_service_server::RepositoryService for GitksService {
request: tonic::Request<RepackRequest>, request: tonic::Request<RepackRequest>,
) -> Result<tonic::Response<RepositoryMaintenanceResponse>, tonic::Status> { ) -> Result<tonic::Response<RepositoryMaintenanceResponse>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("repo.repack", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
Ok(tonic::Response::new(repository_maint::run_repack( let resp = repository_maint::run_repack(
&gb, &gb,
inner.full, inner.full,
inner.write_bitmaps, inner.write_bitmaps,
inner.write_multi_pack_index, inner.write_multi_pack_index,
)?)) )?;
tracing::info!(%repo, ok = resp.ok, "repack done");
Ok(tonic::Response::new(resp))
} }
async fn write_commit_graph( async fn write_commit_graph(
@@ -226,9 +269,12 @@ impl repository_service_server::RepositoryService for GitksService {
request: tonic::Request<WriteCommitGraphRequest>, request: tonic::Request<WriteCommitGraphRequest>,
) -> Result<tonic::Response<RepositoryMaintenanceResponse>, tonic::Status> { ) -> Result<tonic::Response<RepositoryMaintenanceResponse>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("repo.write_commit_graph", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
Ok(tonic::Response::new( let resp = repository_maint::run_commit_graph_write(&gb, inner.split, inner.replace)?;
repository_maint::run_commit_graph_write(&gb, inner.split, inner.replace)?, tracing::info!(%repo, ok = resp.ok, "commit-graph write done");
)) Ok(tonic::Response::new(resp))
} }
} }
+49 -12
View File
@@ -10,19 +10,32 @@ pub(crate) fn maintenance_response(out: std::process::Output) -> RepositoryMaint
} }
} }
fn dir_size(path: &std::path::Path) -> u64 { /// Get approximate repository size using git count-objects instead of
let mut total = 0u64; /// recursively scanning the filesystem (which is O(n) and very slow for large repos).
if let Ok(entries) = std::fs::read_dir(path) { fn dir_size(gb: &crate::bare::GitBare) -> u64 {
for entry in entries.flatten() { let out = git_cmd(gb, &["count-objects", "-v"]).ok();
let p = entry.path(); let text = out
if p.is_file() { .as_ref()
total += entry.metadata().map(|m| m.len()).unwrap_or(0); .map(|o| String::from_utf8_lossy(&o.stdout).into_owned())
} else if p.is_dir() { .unwrap_or_default();
total += dir_size(&p);
let mut loose_size_kb = 0u64;
let mut pack_size_kb = 0u64;
let mut garbage_size_kb = 0u64;
for line in text.lines() {
let line = line.trim();
if let Some(val) = line.strip_prefix("size: ") {
loose_size_kb = val.trim().parse().unwrap_or(0);
} else if let Some(val) = line.strip_prefix("size-pack: ") {
pack_size_kb = val.trim().parse().unwrap_or(0);
} else if let Some(val) = line.strip_prefix("size-garbage: ") {
garbage_size_kb = val.trim().parse().unwrap_or(0);
} }
} }
}
total // count-objects reports sizes in KiB; convert to bytes
(loose_size_kb + pack_size_kb + garbage_size_kb) * 1024
} }
fn count_refs(gb: &crate::bare::GitBare) -> u64 { fn count_refs(gb: &crate::bare::GitBare) -> u64 {
@@ -44,7 +57,7 @@ fn file_len(path: &std::path::Path) -> u64 {
} }
pub(crate) fn get_statistics(gb: &crate::bare::GitBare) -> RepositoryStatistics { pub(crate) fn get_statistics(gb: &crate::bare::GitBare) -> RepositoryStatistics {
let size_bytes = dir_size(&gb.bare_dir); let size_bytes = dir_size(gb);
let mut loose_object_count: u64 = 0; let mut loose_object_count: u64 = 0;
let mut packed_object_count: u64 = 0; let mut packed_object_count: u64 = 0;
@@ -81,6 +94,11 @@ pub(crate) fn check_health(
gb: &crate::bare::GitBare, gb: &crate::bare::GitBare,
connectivity_only: bool, connectivity_only: bool,
) -> Result<RepositoryHealthResponse, tonic::Status> { ) -> Result<RepositoryHealthResponse, tonic::Status> {
tracing::info!(
repo = %gb.bare_dir.display(),
connectivity_only = connectivity_only,
"running health check"
);
let mut args: Vec<&str> = vec!["fsck"]; let mut args: Vec<&str> = vec!["fsck"];
if connectivity_only { if connectivity_only {
args.push("--connectivity-only"); args.push("--connectivity-only");
@@ -109,6 +127,12 @@ pub(crate) fn run_gc(
prune: bool, prune: bool,
aggressive: bool, aggressive: bool,
) -> Result<RepositoryMaintenanceResponse, tonic::Status> { ) -> Result<RepositoryMaintenanceResponse, tonic::Status> {
tracing::info!(
repo = %gb.bare_dir.display(),
prune = prune,
aggressive = aggressive,
"running garbage collection"
);
let mut args: Vec<&str> = vec!["gc"]; let mut args: Vec<&str> = vec!["gc"];
if prune { if prune {
args.push("--prune=now"); args.push("--prune=now");
@@ -126,6 +150,13 @@ pub(crate) fn run_repack(
write_bitmaps: bool, write_bitmaps: bool,
write_multi_pack_index: bool, write_multi_pack_index: bool,
) -> Result<RepositoryMaintenanceResponse, tonic::Status> { ) -> Result<RepositoryMaintenanceResponse, tonic::Status> {
tracing::info!(
repo = %gb.bare_dir.display(),
full = full,
write_bitmaps = write_bitmaps,
write_multi_pack_index = write_multi_pack_index,
"running repack"
);
let mut args: Vec<&str> = vec!["repack", "-d"]; let mut args: Vec<&str> = vec!["repack", "-d"];
if full { if full {
args.push("-a"); args.push("-a");
@@ -145,6 +176,12 @@ pub(crate) fn run_commit_graph_write(
split: bool, split: bool,
replace: bool, replace: bool,
) -> Result<RepositoryMaintenanceResponse, tonic::Status> { ) -> Result<RepositoryMaintenanceResponse, tonic::Status> {
tracing::info!(
repo = %gb.bare_dir.display(),
split = split,
replace = replace,
"writing commit-graph"
);
let mut args: Vec<&str> = vec!["commit-graph", "write"]; let mut args: Vec<&str> = vec!["commit-graph", "write"];
if split { if split {
args.push("--split"); args.push("--split");
+23
View File
@@ -9,8 +9,12 @@ impl tag_service_server::TagService for GitksService {
request: tonic::Request<ListTagsRequest>, request: tonic::Request<ListTagsRequest>,
) -> Result<tonic::Response<ListTagsResponse>, tonic::Status> { ) -> Result<tonic::Response<ListTagsResponse>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("tag.list_tags", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.list_tags(inner).map_err(into_status)?; let resp = gb.list_tags(inner).map_err(into_status)?;
tracing::info!(%repo, count = resp.tags.len(), "list_tags done");
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
@@ -19,6 +23,10 @@ impl tag_service_server::TagService for GitksService {
request: tonic::Request<GetTagRequest>, request: tonic::Request<GetTagRequest>,
) -> Result<tonic::Response<Tag>, tonic::Status> { ) -> Result<tonic::Response<Tag>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let name = inner.name.clone();
let span = tracing::info_span!("tag.get_tag", %repo, %name);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.get_tag(inner).map_err(into_status)?; let resp = gb.get_tag(inner).map_err(into_status)?;
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
@@ -29,8 +37,13 @@ impl tag_service_server::TagService for GitksService {
request: tonic::Request<CreateTagRequest>, request: tonic::Request<CreateTagRequest>,
) -> Result<tonic::Response<Tag>, tonic::Status> { ) -> Result<tonic::Response<Tag>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let name = inner.name.clone();
let span = tracing::info_span!("tag.create_tag", %repo, %name);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.create_tag(inner).map_err(into_status)?; let resp = gb.create_tag(inner).map_err(into_status)?;
tracing::info!(%repo, %name, "tag created");
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
@@ -39,8 +52,13 @@ impl tag_service_server::TagService for GitksService {
request: tonic::Request<DeleteTagRequest>, request: tonic::Request<DeleteTagRequest>,
) -> Result<tonic::Response<()>, tonic::Status> { ) -> Result<tonic::Response<()>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let name = inner.name.clone();
let span = tracing::info_span!("tag.delete_tag", %repo, %name);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
gb.delete_tag(inner).map_err(into_status)?; gb.delete_tag(inner).map_err(into_status)?;
tracing::info!(%repo, %name, "tag deleted");
Ok(tonic::Response::new(())) Ok(tonic::Response::new(()))
} }
@@ -49,8 +67,13 @@ impl tag_service_server::TagService for GitksService {
request: tonic::Request<VerifyTagRequest>, request: tonic::Request<VerifyTagRequest>,
) -> Result<tonic::Response<VerifiedSignature>, tonic::Status> { ) -> Result<tonic::Response<VerifiedSignature>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let name = inner.name.clone();
let span = tracing::info_span!("tag.verify_tag", %repo, %name);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.verify_tag(inner).map_err(into_status)?; let resp = gb.verify_tag(inner).map_err(into_status)?;
tracing::info!(%repo, %name, verified = resp.verified, "tag verified");
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
} }
+68 -7
View File
@@ -1,6 +1,6 @@
use crate::pb::*; use crate::pb::*;
use super::{GitksService, into_status, into_stream}; use super::{GitksService, cache, into_status, into_stream};
#[tonic::async_trait] #[tonic::async_trait]
impl tree_service_server::TreeService for GitksService { impl tree_service_server::TreeService for GitksService {
@@ -12,8 +12,18 @@ impl tree_service_server::TreeService for GitksService {
request: tonic::Request<ListTreeRequest>, request: tonic::Request<ListTreeRequest>,
) -> Result<tonic::Response<ListTreeResponse>, tonic::Status> { ) -> Result<tonic::Response<ListTreeResponse>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("tree.list_tree", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.list_tree(inner).map_err(into_status)?; let resp = if cache::selector_is_oid(&inner.revision) {
cache::cached_response("tree.list_tree", &inner, || {
gb.list_tree(inner.clone()).map_err(into_status)
})?
} else {
gb.list_tree(inner).map_err(into_status)?
};
tracing::info!(%repo, count = resp.entries.len(), "list_tree done");
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
@@ -22,8 +32,17 @@ impl tree_service_server::TreeService for GitksService {
request: tonic::Request<GetTreeRequest>, request: tonic::Request<GetTreeRequest>,
) -> Result<tonic::Response<Tree>, tonic::Status> { ) -> Result<tonic::Response<Tree>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("tree.get_tree", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.get_tree(inner).map_err(into_status)?; let resp = if cache::selector_is_oid(&inner.revision) {
cache::cached_response("tree.get_tree", &inner, || {
gb.get_tree(inner.clone()).map_err(into_status)
})?
} else {
gb.get_tree(inner).map_err(into_status)?
};
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
@@ -32,8 +51,18 @@ impl tree_service_server::TreeService for GitksService {
request: tonic::Request<GetBlobRequest>, request: tonic::Request<GetBlobRequest>,
) -> Result<tonic::Response<Blob>, tonic::Status> { ) -> Result<tonic::Response<Blob>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let path = inner.path.clone();
let span = tracing::info_span!("tree.get_blob", %repo, %path);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.get_blob(inner).map_err(into_status)?; let resp = if cache::selector_is_oid(&inner.revision) {
cache::cached_response("tree.get_blob", &inner, || {
gb.get_blob(inner.clone()).map_err(into_status)
})?
} else {
gb.get_blob(inner).map_err(into_status)?
};
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
@@ -42,8 +71,21 @@ impl tree_service_server::TreeService for GitksService {
request: tonic::Request<GetRawBlobRequest>, request: tonic::Request<GetRawBlobRequest>,
) -> Result<tonic::Response<Self::GetRawBlobStream>, tonic::Status> { ) -> Result<tonic::Response<Self::GetRawBlobStream>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("tree.get_raw_blob", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let items = gb.get_raw_blob(inner).map_err(into_status)?; let items = if inner.oid.is_some() {
cache::cached_vec_response("tree.get_raw_blob", &inner, || {
gb.get_raw_blob(inner.clone()).map_err(into_status)
})?
} else if cache::selector_is_oid(&inner.revision) {
cache::cached_vec_response("tree.get_raw_blob", &inner, || {
gb.get_raw_blob(inner.clone()).map_err(into_status)
})?
} else {
gb.get_raw_blob(inner).map_err(into_status)?
};
Ok(tonic::Response::new(into_stream(items))) Ok(tonic::Response::new(into_stream(items)))
} }
@@ -52,8 +94,17 @@ impl tree_service_server::TreeService for GitksService {
request: tonic::Request<GetFileMetadataRequest>, request: tonic::Request<GetFileMetadataRequest>,
) -> Result<tonic::Response<FileMetadata>, tonic::Status> { ) -> Result<tonic::Response<FileMetadata>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("tree.get_file_metadata", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.get_file_metadata(inner).map_err(into_status)?; let resp = if cache::selector_is_oid(&inner.revision) {
cache::cached_response("tree.get_file_metadata", &inner, || {
gb.get_file_metadata(inner.clone()).map_err(into_status)
})?
} else {
gb.get_file_metadata(inner).map_err(into_status)?
};
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
@@ -62,8 +113,18 @@ impl tree_service_server::TreeService for GitksService {
request: tonic::Request<FindFilesRequest>, request: tonic::Request<FindFilesRequest>,
) -> Result<tonic::Response<FindFilesResponse>, tonic::Status> { ) -> Result<tonic::Response<FindFilesResponse>, tonic::Status> {
let inner = request.into_inner(); let inner = request.into_inner();
let repo = self.repo_label(inner.repository.as_ref());
let span = tracing::info_span!("tree.find_files", %repo);
let _enter = span.enter();
let gb = self.resolve(inner.repository.as_ref())?; let gb = self.resolve(inner.repository.as_ref())?;
let resp = gb.find_files(inner).map_err(into_status)?; let resp = if cache::selector_is_oid(&inner.revision) {
cache::cached_response("tree.find_files", &inner, || {
gb.find_files(inner.clone()).map_err(into_status)
})?
} else {
gb.find_files(inner).map_err(into_status)?
};
tracing::info!(%repo, count = resp.files.len(), "find_files done");
Ok(tonic::Response::new(resp)) Ok(tonic::Response::new(resp))
} }
} }
+123 -58
View File
@@ -1,13 +1,23 @@
mod common; mod common;
use gitks::pb::archive_service_server::ArchiveService;
use gitks::pb::pack_service_server::PackService;
use gitks::pb::*; use gitks::pb::*;
#[test] fn hdr(name: &str) -> RepositoryHeader {
fn test_get_archive_tar() { RepositoryHeader {
let (_dir, gb) = common::setup_bare_repo(); relative_path: name.into(),
let chunks = gb ..Default::default()
.get_archive(ArchiveRequest { }
repository: None, }
#[tokio::test]
async fn test_get_archive_tar() {
let (dir, gb) = common::setup_bare_repo();
let svc = common::setup_service(dir.path());
let chunks = svc
.get_archive(tonic::Request::new(ArchiveRequest {
repository: Some(hdr("test-repo")),
treeish: Some(ObjectSelector { treeish: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main".into(), revision: "main".into(),
@@ -17,20 +27,24 @@ fn test_get_archive_tar() {
format: archive_options::Format::ArchiveFormatTar as i32, format: archive_options::Format::ArchiveFormatTar as i32,
..Default::default() ..Default::default()
}), }),
}) }))
.expect("get_archive tar"); .await
.unwrap()
.into_inner();
let chunks: Vec<_> = tokio_stream::StreamExt::collect(chunks).await;
assert!(!chunks.is_empty(), "should produce archive data"); assert!(!chunks.is_empty(), "should produce archive data");
let total_size: usize = chunks.iter().map(|c| c.data.len()).sum(); let total_size: usize = chunks.iter().map(|c| c.as_ref().unwrap().data.len()).sum();
assert!(total_size > 0, "archive should not be empty"); assert!(total_size > 0, "archive should not be empty");
} }
#[test] #[tokio::test]
fn test_get_archive_zip() { async fn test_get_archive_zip() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, gb) = common::setup_bare_repo();
let chunks = gb let svc = common::setup_service(dir.path());
.get_archive(ArchiveRequest { let chunks = svc
repository: None, .get_archive(tonic::Request::new(ArchiveRequest {
repository: Some(hdr("test-repo")),
treeish: Some(ObjectSelector { treeish: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main".into(), revision: "main".into(),
@@ -40,23 +54,27 @@ fn test_get_archive_zip() {
format: archive_options::Format::ArchiveFormatZip as i32, format: archive_options::Format::ArchiveFormatZip as i32,
..Default::default() ..Default::default()
}), }),
}) }))
.expect("get_archive zip"); .await
.unwrap()
.into_inner();
let chunks: Vec<_> = tokio_stream::StreamExt::collect(chunks).await;
assert!(!chunks.is_empty()); assert!(!chunks.is_empty());
let data = &chunks[0].data; let data = &chunks[0].as_ref().unwrap().data;
assert!( assert!(
data.starts_with(b"PK"), data.starts_with(b"PK"),
"zip archive should start with PK magic bytes" "zip archive should start with PK magic bytes"
); );
} }
#[test] #[tokio::test]
fn test_list_archive_entries() { async fn test_list_archive_entries() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, gb) = common::setup_bare_repo();
let result = gb let svc = common::setup_service(dir.path());
.list_archive_entries(ListArchiveEntriesRequest { let result = svc
repository: None, .list_archive_entries(tonic::Request::new(ListArchiveEntriesRequest {
repository: Some(hdr("test-repo")),
treeish: Some(ObjectSelector { treeish: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main".into(), revision: "main".into(),
@@ -64,8 +82,10 @@ fn test_list_archive_entries() {
}), }),
pathspec: vec![], pathspec: vec![],
pagination: None, pagination: None,
}) }))
.expect("list_archive_entries"); .await
.unwrap()
.into_inner();
assert!(!result.entries.is_empty(), "should list entries"); assert!(!result.entries.is_empty(), "should list entries");
let paths: Vec<&str> = result.entries.iter().map(|e| e.path.as_str()).collect(); let paths: Vec<&str> = result.entries.iter().map(|e| e.path.as_str()).collect();
@@ -76,12 +96,13 @@ fn test_list_archive_entries() {
); );
} }
#[test] #[tokio::test]
fn test_get_archive_with_prefix() { async fn test_get_archive_with_prefix() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, gb) = common::setup_bare_repo();
let chunks = gb let svc = common::setup_service(dir.path());
.get_archive(ArchiveRequest { let chunks = svc
repository: None, .get_archive(tonic::Request::new(ArchiveRequest {
repository: Some(hdr("test-repo")),
treeish: Some(ObjectSelector { treeish: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main".into(), revision: "main".into(),
@@ -92,29 +113,68 @@ fn test_get_archive_with_prefix() {
prefix: "project/".into(), prefix: "project/".into(),
..Default::default() ..Default::default()
}), }),
}) }))
.expect("get_archive with prefix"); .await
.unwrap()
.into_inner();
let chunks: Vec<_> = tokio_stream::StreamExt::collect(chunks).await;
assert!(!chunks.is_empty()); assert!(!chunks.is_empty());
} }
#[test] #[tokio::test]
fn test_fsck_clean_repo() { async fn test_fsck_clean_repo() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, gb) = common::setup_bare_repo();
let result = gb let svc = common::setup_service(dir.path());
.fsck(FsckRequest { let result = svc
repository: None, .fsck(tonic::Request::new(FsckRequest {
repository: Some(hdr("test-repo")),
strict: false, strict: false,
connectivity_only: false, connectivity_only: false,
}) }))
.expect("fsck"); .await
.unwrap()
.into_inner();
assert!(result.ok); assert!(result.ok);
assert!(result.errors.is_empty()); assert!(result.errors.is_empty());
} }
#[test] #[tokio::test]
fn test_list_packfiles() { async fn test_fsck_strict() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let svc = common::setup_service(dir.path());
let result = svc
.fsck(tonic::Request::new(FsckRequest {
repository: Some(hdr("test-repo")),
strict: true,
connectivity_only: false,
}))
.await
.unwrap()
.into_inner();
assert!(result.ok, "strict fsck should pass on clean repo");
}
#[tokio::test]
async fn test_fsck_connectivity_only() {
let (dir, _gb) = common::setup_bare_repo();
let svc = common::setup_service(dir.path());
let result = svc
.fsck(tonic::Request::new(FsckRequest {
repository: Some(hdr("test-repo")),
strict: false,
connectivity_only: true,
}))
.await
.unwrap()
.into_inner();
assert!(result.ok, "connectivity-only fsck should pass");
}
#[tokio::test]
async fn test_list_packfiles() {
let (dir, gb) = common::setup_bare_repo();
let svc = common::setup_service(dir.path());
duct::cmd( duct::cmd(
"git", "git",
@@ -128,12 +188,14 @@ fn test_list_packfiles() {
.run() .run()
.expect("git gc"); .expect("git gc");
let result = gb let result = svc
.list_packfiles(ListPackfilesRequest { .list_packfiles(tonic::Request::new(ListPackfilesRequest {
repository: None, repository: Some(hdr("test-repo")),
pagination: None, pagination: None,
}) }))
.expect("list_packfiles"); .await
.unwrap()
.into_inner();
assert!( assert!(
!result.packfiles.is_empty(), !result.packfiles.is_empty(),
@@ -145,16 +207,19 @@ fn test_list_packfiles() {
} }
} }
#[test] #[tokio::test]
fn test_advertise_refs() { async fn test_advertise_refs() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, gb) = common::setup_bare_repo();
let result = gb let svc = common::setup_service(dir.path());
.advertise_refs(AdvertiseRefsRequest { let result = svc
repository: None, .advertise_refs(tonic::Request::new(AdvertiseRefsRequest {
repository: Some(hdr("test-repo")),
protocol: None, protocol: None,
service: String::new(), service: String::new(),
}) }))
.expect("advertise_refs"); .await
.unwrap()
.into_inner();
assert!(!result.references.is_empty(), "should have refs"); assert!(!result.references.is_empty(), "should have refs");
let ref_names: Vec<&str> = result.references.iter().map(|r| r.name.as_str()).collect(); let ref_names: Vec<&str> = result.references.iter().map(|r| r.name.as_str()).collect();
+91 -38
View File
@@ -1,13 +1,22 @@
mod common; mod common;
use gitks::pb::blame_service_server::BlameService;
use gitks::pb::*; use gitks::pb::*;
#[test] fn hdr() -> RepositoryHeader {
fn test_blame_basic() { RepositoryHeader {
let (_dir, gb) = common::setup_bare_repo(); relative_path: "test-repo".into(),
let result = gb ..Default::default()
.blame(BlameRequest { }
repository: None, }
#[tokio::test]
async fn test_blame_basic() {
let (dir, _gb) = common::setup_bare_repo();
let svc = common::setup_service(dir.path());
let result = svc
.blame(tonic::Request::new(BlameRequest {
repository: Some(hdr()),
revision: Some(ObjectSelector { revision: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main".into(), revision: "main".into(),
@@ -17,8 +26,10 @@ fn test_blame_basic() {
range: None, range: None,
options: None, options: None,
pagination: None, pagination: None,
}) }))
.expect("blame"); .await
.unwrap()
.into_inner();
assert!(!result.hunks.is_empty(), "should have blame hunks"); assert!(!result.hunks.is_empty(), "should have blame hunks");
for hunk in &result.hunks { for hunk in &result.hunks {
@@ -31,12 +42,13 @@ fn test_blame_basic() {
} }
} }
#[test] #[tokio::test]
fn test_blame_line_content() { async fn test_blame_line_content() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let result = gb let svc = common::setup_service(dir.path());
.blame(BlameRequest { let result = svc
repository: None, .blame(tonic::Request::new(BlameRequest {
repository: Some(hdr()),
revision: Some(ObjectSelector { revision: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main".into(), revision: "main".into(),
@@ -46,8 +58,10 @@ fn test_blame_line_content() {
range: None, range: None,
options: None, options: None,
pagination: None, pagination: None,
}) }))
.expect("blame"); .await
.unwrap()
.into_inner();
let all_lines: Vec<String> = result let all_lines: Vec<String> = result
.hunks .hunks
@@ -63,12 +77,13 @@ fn test_blame_line_content() {
); );
} }
#[test] #[tokio::test]
fn test_blame_with_range() { async fn test_blame_with_range() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let result = gb let svc = common::setup_service(dir.path());
.blame(BlameRequest { let result = svc
repository: None, .blame(tonic::Request::new(BlameRequest {
repository: Some(hdr()),
revision: Some(ObjectSelector { revision: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main".into(), revision: "main".into(),
@@ -78,18 +93,21 @@ fn test_blame_with_range() {
range: Some(LineRange { start: 1, end: 1 }), range: Some(LineRange { start: 1, end: 1 }),
options: None, options: None,
pagination: None, pagination: None,
}) }))
.expect("blame with range"); .await
.unwrap()
.into_inner();
assert!(!result.hunks.is_empty(), "should have hunks for range"); assert!(!result.hunks.is_empty(), "should have hunks for range");
} }
#[test] #[tokio::test]
fn test_blame_author_info() { async fn test_blame_author_info() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let result = gb let svc = common::setup_service(dir.path());
.blame(BlameRequest { let result = svc
repository: None, .blame(tonic::Request::new(BlameRequest {
repository: Some(hdr()),
revision: Some(ObjectSelector { revision: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main".into(), revision: "main".into(),
@@ -99,8 +117,10 @@ fn test_blame_author_info() {
range: None, range: None,
options: None, options: None,
pagination: None, pagination: None,
}) }))
.expect("blame"); .await
.unwrap()
.into_inner();
let hunk = &result.hunks[0]; let hunk = &result.hunks[0];
let commit = hunk.commit.as_ref().unwrap(); let commit = hunk.commit.as_ref().unwrap();
@@ -112,11 +132,13 @@ fn test_blame_author_info() {
} }
} }
#[test] #[tokio::test]
fn test_blame_nonexistent_file() { async fn test_blame_nonexistent_file() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let result = gb.blame(BlameRequest { let svc = common::setup_service(dir.path());
repository: None, let result = svc
.blame(tonic::Request::new(BlameRequest {
repository: Some(hdr()),
revision: Some(ObjectSelector { revision: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main".into(), revision: "main".into(),
@@ -126,7 +148,38 @@ fn test_blame_nonexistent_file() {
range: None, range: None,
options: None, options: None,
pagination: None, pagination: None,
}); }))
.await;
assert!(result.is_err(), "blame on nonexistent file should fail"); assert!(result.is_err(), "blame on nonexistent file should fail");
} }
#[tokio::test]
async fn test_stream_blame() {
let (dir, _gb) = common::setup_bare_repo();
let svc = common::setup_service(dir.path());
let stream = svc
.stream_blame(tonic::Request::new(BlameRequest {
repository: Some(hdr()),
revision: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main".into(),
})),
}),
path: "README.md".into(),
range: None,
options: None,
pagination: None,
}))
.await
.unwrap()
.into_inner();
let hunks: Vec<_> = tokio_stream::StreamExt::collect(stream).await;
assert!(!hunks.is_empty(), "stream should produce blame hunks");
for hunk in &hunks {
let hunk = hunk.as_ref().unwrap();
assert!(hunk.commit.is_some());
assert!(hunk.line_count > 0);
}
}
+195 -80
View File
@@ -1,51 +1,70 @@
mod common; mod common;
use gitks::pb::branch_service_server::BranchService;
use gitks::pb::*; use gitks::pb::*;
#[test] #[allow(unused_imports)]
fn test_list_branches() { use gitks::pb::{BranchUpstream, SetBranchUpstreamRequest, UpdateBranchTargetRequest};
let (_dir, gb) = common::setup_bare_repo();
let result = gb fn hdr() -> RepositoryHeader {
.list_branches(ListBranchesRequest { RepositoryHeader {
repository: None, relative_path: "test-repo".into(),
..Default::default()
}
}
#[tokio::test]
async fn test_list_branches() {
let (dir, _gb) = common::setup_bare_repo();
let svc = common::setup_service(dir.path());
let result = svc
.list_branches(tonic::Request::new(ListBranchesRequest {
repository: Some(hdr()),
pattern: String::new(), pattern: String::new(),
merged_into_head: false, merged_into_head: false,
not_merged_into_head: false, not_merged_into_head: false,
pagination: None, pagination: None,
sort_direction: 0, sort_direction: 0,
}) }))
.expect("list_branches"); .await
.unwrap()
.into_inner();
let names: Vec<String> = result.branches.iter().map(|b| b.name.clone()).collect(); let names: Vec<String> = result.branches.iter().map(|b| b.name.clone()).collect();
assert!(names.contains(&"feature".to_string())); assert!(names.contains(&"feature".to_string()));
assert!(names.contains(&"main".to_string())); assert!(names.contains(&"main".to_string()));
assert!(result.branches.len() >= 2); assert!(result.branches.len() >= 2);
} }
#[test] #[tokio::test]
fn test_list_branches_merged_filter() { async fn test_list_branches_merged_filter() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let svc = common::setup_service(dir.path());
let merged = gb let merged = svc
.list_branches(ListBranchesRequest { .list_branches(tonic::Request::new(ListBranchesRequest {
repository: None, repository: Some(hdr()),
pattern: String::new(), pattern: String::new(),
merged_into_head: true, merged_into_head: true,
not_merged_into_head: false, not_merged_into_head: false,
pagination: None, pagination: None,
sort_direction: 0, sort_direction: 0,
}) }))
.expect("list_branches merged"); .await
.unwrap()
.into_inner();
let not_merged = gb let not_merged = svc
.list_branches(ListBranchesRequest { .list_branches(tonic::Request::new(ListBranchesRequest {
repository: None, repository: Some(hdr()),
pattern: String::new(), pattern: String::new(),
merged_into_head: false, merged_into_head: false,
not_merged_into_head: true, not_merged_into_head: true,
pagination: None, pagination: None,
sort_direction: 0, sort_direction: 0,
}) }))
.expect("list_branches not merged"); .await
.unwrap()
.into_inner();
let merged_names: Vec<&str> = merged.branches.iter().map(|b| b.name.as_str()).collect(); let merged_names: Vec<&str> = merged.branches.iter().map(|b| b.name.as_str()).collect();
let not_merged_names: Vec<&str> = not_merged let not_merged_names: Vec<&str> = not_merged
@@ -66,15 +85,18 @@ fn test_list_branches_merged_filter() {
); );
} }
#[test] #[tokio::test]
fn test_get_branch() { async fn test_get_branch() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let branch = gb let svc = common::setup_service(dir.path());
.get_branch(GetBranchRequest { let branch = svc
repository: None, .get_branch(tonic::Request::new(GetBranchRequest {
repository: Some(hdr()),
name: "feature".into(), name: "feature".into(),
}) }))
.expect("get_branch"); .await
.unwrap()
.into_inner();
assert_eq!(branch.full_ref, "refs/heads/feature"); assert_eq!(branch.full_ref, "refs/heads/feature");
let oid = branch.target_oid.unwrap(); let oid = branch.target_oid.unwrap();
assert!(!oid.value.is_empty()); assert!(!oid.value.is_empty());
@@ -82,12 +104,13 @@ fn test_get_branch() {
assert_eq!(oid.hex.len(), 40); assert_eq!(oid.hex.len(), 40);
} }
#[test] #[tokio::test]
fn test_branch_pagination() { async fn test_branch_pagination() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let result = gb let svc = common::setup_service(dir.path());
.list_branches(ListBranchesRequest { let result = svc
repository: None, .list_branches(tonic::Request::new(ListBranchesRequest {
repository: Some(hdr()),
pattern: String::new(), pattern: String::new(),
merged_into_head: false, merged_into_head: false,
not_merged_into_head: false, not_merged_into_head: false,
@@ -96,15 +119,17 @@ fn test_branch_pagination() {
page_token: String::new(), page_token: String::new(),
}), }),
sort_direction: 0, sort_direction: 0,
}) }))
.expect("list_branches page 1"); .await
.unwrap()
.into_inner();
let page_info = result.page_info.unwrap(); let page_info = result.page_info.unwrap();
assert_eq!(result.branches.len(), 1); assert_eq!(result.branches.len(), 1);
assert!(page_info.has_next_page); assert!(page_info.has_next_page);
let result2 = gb let result2 = svc
.list_branches(ListBranchesRequest { .list_branches(tonic::Request::new(ListBranchesRequest {
repository: None, repository: Some(hdr()),
pattern: String::new(), pattern: String::new(),
merged_into_head: false, merged_into_head: false,
not_merged_into_head: false, not_merged_into_head: false,
@@ -113,18 +138,21 @@ fn test_branch_pagination() {
page_token: page_info.next_page_token, page_token: page_info.next_page_token,
}), }),
sort_direction: 0, sort_direction: 0,
}) }))
.expect("list_branches page 2"); .await
.unwrap()
.into_inner();
assert!(!result2.branches.is_empty()); assert!(!result2.branches.is_empty());
assert_ne!(result.branches[0].name, result2.branches[0].name); assert_ne!(result.branches[0].name, result2.branches[0].name);
} }
#[test] #[tokio::test]
fn test_create_and_delete_branch() { async fn test_create_and_delete_branch() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let branch = gb let svc = common::setup_service(dir.path());
.create_branch(CreateBranchRequest { let branch = svc
repository: None, .create_branch(tonic::Request::new(CreateBranchRequest {
repository: Some(hdr()),
name: "new-branch".into(), name: "new-branch".into(),
start_point: Some(ObjectSelector { start_point: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
@@ -132,29 +160,35 @@ fn test_create_and_delete_branch() {
})), })),
}), }),
force: false, force: false,
}) }))
.expect("create_branch"); .await
.unwrap()
.into_inner();
assert_eq!(branch.name, "new-branch"); assert_eq!(branch.name, "new-branch");
gb.delete_branch(DeleteBranchRequest { svc.delete_branch(tonic::Request::new(DeleteBranchRequest {
repository: None, repository: Some(hdr()),
name: "new-branch".into(), name: "new-branch".into(),
force: true, force: true,
}) }))
.expect("delete_branch"); .await
.unwrap();
let result = gb.get_branch(GetBranchRequest { let result = svc
repository: None, .get_branch(tonic::Request::new(GetBranchRequest {
repository: Some(hdr()),
name: "new-branch".into(), name: "new-branch".into(),
}); }))
.await;
assert!(result.is_err(), "deleted branch should not exist"); assert!(result.is_err(), "deleted branch should not exist");
} }
#[test] #[tokio::test]
fn test_rename_branch() { async fn test_rename_branch() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
gb.create_branch(CreateBranchRequest { let svc = common::setup_service(dir.path());
repository: None, svc.create_branch(tonic::Request::new(CreateBranchRequest {
repository: Some(hdr()),
name: "to-rename".into(), name: "to-rename".into(),
start_point: Some(ObjectSelector { start_point: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
@@ -162,35 +196,43 @@ fn test_rename_branch() {
})), })),
}), }),
force: false, force: false,
}) }))
.expect("create branch for rename"); .await
.unwrap();
let renamed = gb let renamed = svc
.rename_branch(RenameBranchRequest { .rename_branch(tonic::Request::new(RenameBranchRequest {
repository: None, repository: Some(hdr()),
old_name: "to-rename".into(), old_name: "to-rename".into(),
new_name: "renamed".into(), new_name: "renamed".into(),
}) }))
.expect("rename_branch"); .await
.unwrap()
.into_inner();
assert_eq!(renamed.name, "renamed"); assert_eq!(renamed.name, "renamed");
let old = gb.get_branch(GetBranchRequest { let old = svc
repository: None, .get_branch(tonic::Request::new(GetBranchRequest {
repository: Some(hdr()),
name: "to-rename".into(), name: "to-rename".into(),
}); }))
.await;
assert!(old.is_err(), "old branch name should not exist"); assert!(old.is_err(), "old branch name should not exist");
} }
#[test] #[tokio::test]
fn test_compare_branch() { async fn test_compare_branch() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let result = gb let svc = common::setup_service(dir.path());
.compare_branch(CompareBranchRequest { let result = svc
repository: None, .compare_branch(tonic::Request::new(CompareBranchRequest {
repository: Some(hdr()),
source_branch: "feature".into(), source_branch: "feature".into(),
target_branch: "main".into(), target_branch: "main".into(),
}) }))
.expect("compare_branch"); .await
.unwrap()
.into_inner();
assert!( assert!(
result.ahead_by > 0 || result.behind_by > 0, result.ahead_by > 0 || result.behind_by > 0,
@@ -198,3 +240,76 @@ fn test_compare_branch() {
); );
assert!(result.merge_base.is_some(), "should find merge base"); assert!(result.merge_base.is_some(), "should find merge base");
} }
#[tokio::test]
async fn test_update_branch_target() {
let (dir, _gb) = common::setup_bare_repo();
let svc = common::setup_service(dir.path());
// Get current main OID
let main_branch = svc
.get_branch(tonic::Request::new(GetBranchRequest {
repository: Some(hdr()),
name: "main".into(),
}))
.await
.unwrap()
.into_inner();
let main_oid = main_branch.target_oid.as_ref().unwrap().clone();
// Create a new branch pointing to main's HEAD
svc.create_branch(tonic::Request::new(CreateBranchRequest {
repository: Some(hdr()),
name: "movable".into(),
start_point: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main~2".into(),
})),
}),
force: false,
}))
.await
.unwrap();
// Update target to main's OID
let updated = svc
.update_branch_target(tonic::Request::new(UpdateBranchTargetRequest {
repository: Some(hdr()),
name: "movable".into(),
expected_old_oid: None,
new_oid: Some(main_oid),
force: true,
}))
.await
.unwrap()
.into_inner();
assert_eq!(updated.name, "movable");
assert_eq!(
updated.target_oid.as_ref().unwrap().hex,
main_branch.target_oid.as_ref().unwrap().hex
);
}
#[tokio::test]
async fn test_set_branch_upstream() {
let (dir, _gb) = common::setup_bare_repo();
let svc = common::setup_service(dir.path());
let result = svc
.set_branch_upstream(tonic::Request::new(SetBranchUpstreamRequest {
repository: Some(hdr()),
name: "main".into(),
upstream: Some(BranchUpstream {
remote_name: "origin".into(),
remote_url: String::new(),
remote_branch_name: "main".into(),
local_branch_name: "main".into(),
}),
}))
.await;
// This may fail if no remote is configured, which is expected
// The important thing is that the code path is exercised
assert!(result.is_ok() || result.is_err());
}
+164 -112
View File
@@ -1,13 +1,23 @@
mod common; mod common;
use gitks::pb::commit_service_server::CommitService;
use gitks::pb::tree_service_server::TreeService;
use gitks::pb::*; use gitks::pb::*;
#[test] fn hdr() -> RepositoryHeader {
fn test_get_commit_with_author() { RepositoryHeader {
let (_dir, gb) = common::setup_bare_repo(); relative_path: "test-repo".into(),
let commit = gb ..Default::default()
.get_commit(GetCommitRequest { }
repository: None, }
#[tokio::test]
async fn test_get_commit_with_author() {
let (dir, _gb) = common::setup_bare_repo();
let svc = common::setup_service(dir.path());
let commit = svc
.get_commit(tonic::Request::new(GetCommitRequest {
repository: Some(hdr()),
revision: Some(ObjectSelector { revision: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main".into(), revision: "main".into(),
@@ -15,8 +25,10 @@ fn test_get_commit_with_author() {
}), }),
include_stats: false, include_stats: false,
include_raw: false, include_raw: false,
}) }))
.expect("get_commit"); .await
.unwrap()
.into_inner();
assert!(commit.author.is_some(), "author must be populated"); assert!(commit.author.is_some(), "author must be populated");
let author = commit.author.as_ref().unwrap(); let author = commit.author.as_ref().unwrap();
@@ -43,12 +55,13 @@ fn test_get_commit_with_author() {
); );
} }
#[test] #[tokio::test]
fn test_get_commit_subject_body() { async fn test_get_commit_subject_body() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let commit = gb let svc = common::setup_service(dir.path());
.get_commit(GetCommitRequest { let commit = svc
repository: None, .get_commit(tonic::Request::new(GetCommitRequest {
repository: Some(hdr()),
revision: Some(ObjectSelector { revision: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main~2".into(), revision: "main~2".into(),
@@ -56,8 +69,10 @@ fn test_get_commit_subject_body() {
}), }),
include_stats: false, include_stats: false,
include_raw: false, include_raw: false,
}) }))
.expect("get_commit"); .await
.unwrap()
.into_inner();
assert_eq!(commit.subject, "second commit"); assert_eq!(commit.subject, "second commit");
assert!(!commit.message.is_empty()); assert!(!commit.message.is_empty());
@@ -65,12 +80,13 @@ fn test_get_commit_subject_body() {
assert!(!commit.parent_oids.is_empty()); assert!(!commit.parent_oids.is_empty());
} }
#[test] #[tokio::test]
fn test_get_commit_with_raw() { async fn test_get_commit_with_raw() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let commit = gb let svc = common::setup_service(dir.path());
.get_commit(GetCommitRequest { let commit = svc
repository: None, .get_commit(tonic::Request::new(GetCommitRequest {
repository: Some(hdr()),
revision: Some(ObjectSelector { revision: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main".into(), revision: "main".into(),
@@ -78,8 +94,10 @@ fn test_get_commit_with_raw() {
}), }),
include_stats: false, include_stats: false,
include_raw: true, include_raw: true,
}) }))
.expect("get_commit with raw"); .await
.unwrap()
.into_inner();
assert!( assert!(
!commit.raw.is_empty(), !commit.raw.is_empty(),
@@ -90,12 +108,13 @@ fn test_get_commit_with_raw() {
assert!(raw_str.contains("author"), "raw should contain author line"); assert!(raw_str.contains("author"), "raw should contain author line");
} }
#[test] #[tokio::test]
fn test_list_commits_with_pagination() { async fn test_list_commits_with_pagination() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let page1 = gb let svc = common::setup_service(dir.path());
.list_commits(ListCommitsRequest { let page1 = svc
repository: None, .list_commits(tonic::Request::new(ListCommitsRequest {
repository: Some(hdr()),
revision: Some(ObjectSelector { revision: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main".into(), revision: "main".into(),
@@ -113,15 +132,17 @@ fn test_list_commits_with_pagination() {
page_size: 2, page_size: 2,
page_token: String::new(), page_token: String::new(),
}), }),
}) }))
.expect("list_commits page 1"); .await
.unwrap()
.into_inner();
assert_eq!(page1.commits.len(), 2); assert_eq!(page1.commits.len(), 2);
let pi = page1.page_info.unwrap(); let pi = page1.page_info.unwrap();
assert!(pi.has_next_page); assert!(pi.has_next_page);
let page2 = gb let page2 = svc
.list_commits(ListCommitsRequest { .list_commits(tonic::Request::new(ListCommitsRequest {
repository: None, repository: Some(hdr()),
revision: Some(ObjectSelector { revision: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main".into(), revision: "main".into(),
@@ -139,18 +160,21 @@ fn test_list_commits_with_pagination() {
page_size: 2, page_size: 2,
page_token: pi.next_page_token, page_token: pi.next_page_token,
}), }),
}) }))
.expect("list_commits page 2"); .await
.unwrap()
.into_inner();
assert!(!page2.commits.is_empty()); assert!(!page2.commits.is_empty());
assert_ne!(page1.commits[0].oid, page2.commits[0].oid); assert_ne!(page1.commits[0].oid, page2.commits[0].oid);
} }
#[test] #[tokio::test]
fn test_get_commit_ancestors_pagination() { async fn test_get_commit_ancestors_pagination() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let page1 = gb let svc = common::setup_service(dir.path());
.get_commit_ancestors(GetCommitAncestorsRequest { let page1 = svc
repository: None, .get_commit_ancestors(tonic::Request::new(GetCommitAncestorsRequest {
repository: Some(hdr()),
revision: Some(ObjectSelector { revision: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main".into(), revision: "main".into(),
@@ -161,8 +185,10 @@ fn test_get_commit_ancestors_pagination() {
page_size: 2, page_size: 2,
page_token: String::new(), page_token: String::new(),
}), }),
}) }))
.expect("ancestors page 1"); .await
.unwrap()
.into_inner();
assert_eq!(page1.commits.len(), 2); assert_eq!(page1.commits.len(), 2);
let pi = page1.page_info.unwrap(); let pi = page1.page_info.unwrap();
@@ -172,9 +198,9 @@ fn test_get_commit_ancestors_pagination() {
"next_page_token must be set" "next_page_token must be set"
); );
let page2 = gb let page2 = svc
.get_commit_ancestors(GetCommitAncestorsRequest { .get_commit_ancestors(tonic::Request::new(GetCommitAncestorsRequest {
repository: None, repository: Some(hdr()),
revision: Some(ObjectSelector { revision: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main".into(), revision: "main".into(),
@@ -185,8 +211,10 @@ fn test_get_commit_ancestors_pagination() {
page_size: 2, page_size: 2,
page_token: pi.next_page_token, page_token: pi.next_page_token,
}), }),
}) }))
.expect("ancestors page 2"); .await
.unwrap()
.into_inner();
assert!(!page2.commits.is_empty(), "page 2 should have commits"); assert!(!page2.commits.is_empty(), "page 2 should have commits");
assert_ne!( assert_ne!(
@@ -195,12 +223,13 @@ fn test_get_commit_ancestors_pagination() {
); );
} }
#[test] #[tokio::test]
fn test_compare_commits() { async fn test_compare_commits() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let result = gb let svc = common::setup_service(dir.path());
.compare_commits(CompareCommitsRequest { let result = svc
repository: None, .compare_commits(tonic::Request::new(CompareCommitsRequest {
repository: Some(hdr()),
base: Some(ObjectSelector { base: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "feature".into(), revision: "feature".into(),
@@ -217,8 +246,10 @@ fn test_compare_commits() {
page_size: 100, page_size: 100,
page_token: String::new(), page_token: String::new(),
}), }),
}) }))
.expect("compare_commits"); .await
.unwrap()
.into_inner();
assert!(!result.commits.is_empty()); assert!(!result.commits.is_empty());
assert!(result.merge_base.is_some()); assert!(result.merge_base.is_some());
@@ -226,13 +257,14 @@ fn test_compare_commits() {
assert!(stats.additions > 0); assert!(stats.additions > 0);
} }
#[test] #[tokio::test]
fn test_create_commit_and_cherry_pick() { async fn test_create_commit_and_cherry_pick() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let svc = common::setup_service(dir.path());
let created = gb let created = svc
.create_commit(CreateCommitRequest { .create_commit(tonic::Request::new(CreateCommitRequest {
repository: None, repository: Some(hdr()),
branch: "feature".into(), branch: "feature".into(),
message: "cherry-pick source".into(), message: "cherry-pick source".into(),
author: Some(Signature { author: Some(Signature {
@@ -265,8 +297,10 @@ fn test_create_commit_and_cherry_pick() {
}), }),
force: false, force: false,
trailers: vec![], trailers: vec![],
}) }))
.expect("create_commit for cherry-pick source"); .await
.unwrap()
.into_inner();
let source_oid = created let source_oid = created
.commit .commit
@@ -278,9 +312,9 @@ fn test_create_commit_and_cherry_pick() {
.hex .hex
.clone(); .clone();
let cp_result = gb let cp_result = svc
.cherry_pick_commit(CherryPickCommitRequest { .cherry_pick_commit(tonic::Request::new(CherryPickCommitRequest {
repository: None, repository: Some(hdr()),
commit: Some(ObjectSelector { commit: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: source_oid.clone(), revision: source_oid.clone(),
@@ -296,15 +330,17 @@ fn test_create_commit_and_cherry_pick() {
}), }),
message: String::new(), message: String::new(),
mainline: 0, mainline: 0,
}) }))
.expect("cherry_pick_commit"); .await
.unwrap()
.into_inner();
let cp_commit = cp_result.commit.unwrap(); let cp_commit = cp_result.commit.unwrap();
assert_eq!(cp_commit.subject, "cherry-pick source"); assert_eq!(cp_commit.subject, "cherry-pick source");
let blob = gb let blob = svc
.get_blob(GetBlobRequest { .get_blob(tonic::Request::new(GetBlobRequest {
repository: None, repository: Some(hdr()),
revision: Some(ObjectSelector { revision: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main".into(), revision: "main".into(),
@@ -313,14 +349,17 @@ fn test_create_commit_and_cherry_pick() {
path: "cp_file.txt".into(), path: "cp_file.txt".into(),
oid: None, oid: None,
max_bytes: 0, max_bytes: 0,
}) }))
.expect("get_blob after cherry-pick"); .await
.unwrap()
.into_inner();
assert_eq!(blob.data, b"cherry pick me"); assert_eq!(blob.data, b"cherry pick me");
} }
#[test] #[tokio::test]
fn test_cherry_pick_root_commit() { async fn test_cherry_pick_root_commit() {
let (dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let svc = common::setup_service(dir.path());
let work_dir = dir.path().join("work"); let work_dir = dir.path().join("work");
common::run(&work_dir, &["checkout", "--orphan", "root-source"]); common::run(&work_dir, &["checkout", "--orphan", "root-source"]);
@@ -338,8 +377,8 @@ fn test_cherry_pick_root_commit() {
.stdout; .stdout;
let root_oid = String::from_utf8(root_oid).unwrap().trim().to_string(); let root_oid = String::from_utf8(root_oid).unwrap().trim().to_string();
gb.cherry_pick_commit(CherryPickCommitRequest { svc.cherry_pick_commit(tonic::Request::new(CherryPickCommitRequest {
repository: None, repository: Some(hdr()),
commit: Some(ObjectSelector { commit: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: root_oid, revision: root_oid,
@@ -349,12 +388,13 @@ fn test_cherry_pick_root_commit() {
committer: None, committer: None,
message: String::new(), message: String::new(),
mainline: 0, mainline: 0,
}) }))
.expect("cherry_pick_commit root"); .await
.unwrap();
let blob = gb let blob = svc
.get_blob(GetBlobRequest { .get_blob(tonic::Request::new(GetBlobRequest {
repository: None, repository: Some(hdr()),
revision: Some(ObjectSelector { revision: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "feature".into(), revision: "feature".into(),
@@ -363,18 +403,21 @@ fn test_cherry_pick_root_commit() {
path: "root_only.txt".into(), path: "root_only.txt".into(),
oid: None, oid: None,
max_bytes: 0, max_bytes: 0,
}) }))
.expect("get root file after cherry-pick"); .await
.unwrap()
.into_inner();
assert_eq!(blob.data, b"from root\n"); assert_eq!(blob.data, b"from root\n");
} }
#[test] #[tokio::test]
fn test_revert_commit() { async fn test_revert_commit() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let svc = common::setup_service(dir.path());
let created = gb let created = svc
.create_commit(CreateCommitRequest { .create_commit(tonic::Request::new(CreateCommitRequest {
repository: None, repository: Some(hdr()),
branch: "main".into(), branch: "main".into(),
message: "to be reverted".into(), message: "to be reverted".into(),
author: None, author: None,
@@ -395,8 +438,10 @@ fn test_revert_commit() {
}), }),
force: false, force: false,
trailers: vec![], trailers: vec![],
}) }))
.expect("create_commit"); .await
.unwrap()
.into_inner();
let to_revert = created let to_revert = created
.commit .commit
@@ -408,9 +453,9 @@ fn test_revert_commit() {
.hex .hex
.clone(); .clone();
let revert_result = gb let revert_result = svc
.revert_commit(RevertCommitRequest { .revert_commit(tonic::Request::new(RevertCommitRequest {
repository: None, repository: Some(hdr()),
commit: Some(ObjectSelector { commit: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: to_revert, revision: to_revert,
@@ -419,8 +464,10 @@ fn test_revert_commit() {
branch: "main".into(), branch: "main".into(),
committer: None, committer: None,
message: String::new(), message: String::new(),
}) }))
.expect("revert_commit"); .await
.unwrap()
.into_inner();
let revert_commit = revert_result.commit.unwrap(); let revert_commit = revert_result.commit.unwrap();
assert!( assert!(
@@ -429,8 +476,9 @@ fn test_revert_commit() {
revert_commit.subject revert_commit.subject
); );
let blob_result = gb.get_blob(GetBlobRequest { let blob_result = svc
repository: None, .get_blob(tonic::Request::new(GetBlobRequest {
repository: Some(hdr()),
revision: Some(ObjectSelector { revision: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main".into(), revision: "main".into(),
@@ -439,19 +487,21 @@ fn test_revert_commit() {
path: "revert_me.txt".into(), path: "revert_me.txt".into(),
oid: None, oid: None,
max_bytes: 0, max_bytes: 0,
}); }))
.await;
assert!( assert!(
blob_result.is_err(), blob_result.is_err(),
"revert_me.txt should be deleted after revert" "revert_me.txt should be deleted after revert"
); );
} }
#[test] #[tokio::test]
fn test_oid_binary_encoding() { async fn test_oid_binary_encoding() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let commit = gb let svc = common::setup_service(dir.path());
.get_commit(GetCommitRequest { let commit = svc
repository: None, .get_commit(tonic::Request::new(GetCommitRequest {
repository: Some(hdr()),
revision: Some(ObjectSelector { revision: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main".into(), revision: "main".into(),
@@ -459,8 +509,10 @@ fn test_oid_binary_encoding() {
}), }),
include_stats: false, include_stats: false,
include_raw: false, include_raw: false,
}) }))
.expect("get_commit"); .await
.unwrap()
.into_inner();
let oid = commit.oid.unwrap(); let oid = commit.oid.unwrap();
assert_eq!(oid.value.len(), 20); assert_eq!(oid.value.len(), 20);
assert_eq!(oid.hex.len(), 40); assert_eq!(oid.hex.len(), 40);
+10 -2
View File
@@ -1,4 +1,12 @@
use gitks::bare::GitBare; use gitks::bare::GitBare;
use gitks::server::GitksService;
/// Create a GitksService with a temp directory as repo_prefix
pub fn setup_service(dir: &std::path::Path) -> GitksService {
GitksService {
repo_prefix: dir.to_path_buf(),
}
}
pub fn run_git(work_dir: &std::path::Path, args: &[&str]) -> duct::Expression { pub fn run_git(work_dir: &std::path::Path, args: &[&str]) -> duct::Expression {
duct::cmd("git", { duct::cmd("git", {
@@ -96,7 +104,7 @@ pub fn setup_bare_repo() -> (tempfile::TempDir, GitBare) {
.run() .run()
.expect("set HEAD to main"); .expect("set HEAD to main");
(dir, GitBare { bare_dir }) (dir, GitBare::new(bare_dir))
} }
pub fn setup_bare_repo_with_conflict() -> (tempfile::TempDir, GitBare) { pub fn setup_bare_repo_with_conflict() -> (tempfile::TempDir, GitBare) {
@@ -163,5 +171,5 @@ pub fn setup_bare_repo_with_conflict() -> (tempfile::TempDir, GitBare) {
.run() .run()
.expect("set HEAD to main"); .expect("set HEAD to main");
(dir, GitBare { bare_dir }) (dir, GitBare::new(bare_dir))
} }
+90 -58
View File
@@ -1,13 +1,23 @@
mod common; mod common;
use gitks::pb::commit_service_server::CommitService;
use gitks::pb::diff_service_server::DiffService;
use gitks::pb::*; use gitks::pb::*;
#[test] fn hdr() -> RepositoryHeader {
fn test_get_diff() { RepositoryHeader {
let (_dir, gb) = common::setup_bare_repo(); relative_path: "test-repo".into(),
let result = gb ..Default::default()
.get_diff(GetDiffRequest { }
repository: None, }
#[tokio::test]
async fn test_get_diff() {
let (dir, _gb) = common::setup_bare_repo();
let svc = common::setup_service(dir.path());
let result = svc
.get_diff(tonic::Request::new(GetDiffRequest {
repository: Some(hdr()),
base: Some(ObjectSelector { base: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main~3".into(), revision: "main~3".into(),
@@ -20,8 +30,10 @@ fn test_get_diff() {
}), }),
options: None, options: None,
pagination: None, pagination: None,
}) }))
.expect("get_diff"); .await
.unwrap()
.into_inner();
assert!(!result.files.is_empty()); assert!(!result.files.is_empty());
let paths: Vec<&str> = result.files.iter().map(|f| f.new_path.as_str()).collect(); let paths: Vec<&str> = result.files.iter().map(|f| f.new_path.as_str()).collect();
@@ -34,12 +46,13 @@ fn test_get_diff() {
assert!(stats.additions > 0 || stats.changed_files > 0); assert!(stats.additions > 0 || stats.changed_files > 0);
} }
#[test] #[tokio::test]
fn test_get_diff_with_patch() { async fn test_get_diff_with_patch() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let result = gb let svc = common::setup_service(dir.path());
.get_diff(GetDiffRequest { let result = svc
repository: None, .get_diff(tonic::Request::new(GetDiffRequest {
repository: Some(hdr()),
base: Some(ObjectSelector { base: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main~1".into(), revision: "main~1".into(),
@@ -56,8 +69,10 @@ fn test_get_diff_with_patch() {
..Default::default() ..Default::default()
}), }),
pagination: None, pagination: None,
}) }))
.expect("get_diff with patch"); .await
.unwrap()
.into_inner();
assert!(!result.files.is_empty()); assert!(!result.files.is_empty());
for file in &result.files { for file in &result.files {
@@ -71,12 +86,13 @@ fn test_get_diff_with_patch() {
} }
} }
#[test] #[tokio::test]
fn test_get_diff_with_rename_detection() { async fn test_get_diff_with_rename_detection() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let svc = common::setup_service(dir.path());
gb.create_commit(CreateCommitRequest { svc.create_commit(tonic::Request::new(CreateCommitRequest {
repository: None, repository: Some(hdr()),
branch: "main".into(), branch: "main".into(),
message: "rename file".into(), message: "rename file".into(),
author: None, author: None,
@@ -108,12 +124,13 @@ fn test_get_diff_with_rename_detection() {
}), }),
force: false, force: false,
trailers: vec![], trailers: vec![],
}) }))
.expect("create rename commit"); .await
.unwrap();
let result = gb let result = svc
.get_diff(GetDiffRequest { .get_diff(tonic::Request::new(GetDiffRequest {
repository: None, repository: Some(hdr()),
base: Some(ObjectSelector { base: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main~1".into(), revision: "main~1".into(),
@@ -129,8 +146,10 @@ fn test_get_diff_with_rename_detection() {
..Default::default() ..Default::default()
}), }),
pagination: None, pagination: None,
}) }))
.expect("get_diff with rename detection"); .await
.unwrap()
.into_inner();
let has_rename = result let has_rename = result
.files .files
@@ -143,12 +162,13 @@ fn test_get_diff_with_rename_detection() {
); );
} }
#[test] #[tokio::test]
fn test_get_commit_diff_root() { async fn test_get_commit_diff_root() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let commits = gb let svc = common::setup_service(dir.path());
.list_commits(ListCommitsRequest { let commits = svc
repository: None, .list_commits(tonic::Request::new(ListCommitsRequest {
repository: Some(hdr()),
revision: Some(ObjectSelector { revision: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main".into(), revision: "main".into(),
@@ -166,13 +186,15 @@ fn test_get_commit_diff_root() {
page_size: 1, page_size: 1,
page_token: String::new(), page_token: String::new(),
}), }),
}) }))
.expect("list_commits for root"); .await
.unwrap()
.into_inner();
let root_oid = commits.commits[0].oid.as_ref().unwrap().hex.clone(); let root_oid = commits.commits[0].oid.as_ref().unwrap().hex.clone();
let result = gb let result = svc
.get_commit_diff(GetCommitDiffRequest { .get_commit_diff(tonic::Request::new(GetCommitDiffRequest {
repository: None, repository: Some(hdr()),
commit: Some(ObjectSelector { commit: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: root_oid, revision: root_oid,
@@ -180,18 +202,21 @@ fn test_get_commit_diff_root() {
}), }),
options: None, options: None,
pagination: None, pagination: None,
}) }))
.expect("get_commit_diff on root"); .await
.unwrap()
.into_inner();
assert!(!result.files.is_empty(), "root commit should have files"); assert!(!result.files.is_empty(), "root commit should have files");
} }
#[test] #[tokio::test]
fn test_get_diff_stats() { async fn test_get_diff_stats() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let stats = gb let svc = common::setup_service(dir.path());
.get_diff_stats(GetDiffStatsRequest { let stats = svc
repository: None, .get_diff_stats(tonic::Request::new(GetDiffStatsRequest {
repository: Some(hdr()),
base: Some(ObjectSelector { base: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main~3".into(), revision: "main~3".into(),
@@ -203,17 +228,20 @@ fn test_get_diff_stats() {
})), })),
}), }),
options: None, options: None,
}) }))
.expect("get_diff_stats"); .await
.unwrap()
.into_inner();
assert!(stats.additions > 0 || stats.changed_files > 0); assert!(stats.additions > 0 || stats.changed_files > 0);
} }
#[test] #[tokio::test]
fn test_get_patch() { async fn test_get_patch() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let patches = gb let svc = common::setup_service(dir.path());
.get_patch(GetPatchRequest { let stream = svc
repository: None, .get_patch(tonic::Request::new(GetPatchRequest {
repository: Some(hdr()),
base: Some(ObjectSelector { base: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main~1".into(), revision: "main~1".into(),
@@ -225,12 +253,16 @@ fn test_get_patch() {
})), })),
}), }),
options: None, options: None,
}) }))
.expect("get_patch"); .await
.unwrap()
.into_inner();
let patches: Vec<_> = tokio_stream::StreamExt::collect(stream).await;
assert!(!patches.is_empty()); assert!(!patches.is_empty());
let combined: String = patches let combined: String = patches
.iter() .iter()
.map(|p| String::from_utf8_lossy(&p.data).to_string()) .map(|p| String::from_utf8_lossy(&p.as_ref().unwrap().data).to_string())
.collect(); .collect();
assert!(combined.contains("diff --git") || combined.contains("@@")); assert!(combined.contains("diff --git") || combined.contains("@@"));
} }
+1 -6
View File
@@ -95,12 +95,7 @@ fn setup_bare_repo() -> (tempfile::TempDir, GitBare) {
&["push", "-f", "origin", "refs/tags/v0.1.0:refs/tags/v0.1.0"], &["push", "-f", "origin", "refs/tags/v0.1.0:refs/tags/v0.1.0"],
); );
( (dir, GitBare::new(bare_dir.clone()))
dir,
GitBare {
bare_dir: bare_dir.clone(),
},
)
} }
#[test] #[test]
+128 -86
View File
@@ -1,13 +1,24 @@
mod common; mod common;
use gitks::pb::commit_service_server::CommitService;
use gitks::pb::merge_service_server::MergeService;
use gitks::pb::tree_service_server::TreeService;
use gitks::pb::*; use gitks::pb::*;
#[test] fn hdr() -> RepositoryHeader {
fn test_check_merge_no_conflict() { RepositoryHeader {
let (_dir, gb) = common::setup_bare_repo(); relative_path: "test-repo".into(),
let result = gb ..Default::default()
.check_merge(CheckMergeRequest { }
repository: None, }
#[tokio::test]
async fn test_check_merge_no_conflict() {
let (dir, _gb) = common::setup_bare_repo();
let svc = common::setup_service(dir.path());
let result = svc
.check_merge(tonic::Request::new(CheckMergeRequest {
repository: Some(hdr()),
target: Some(ObjectSelector { target: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main".into(), revision: "main".into(),
@@ -19,8 +30,10 @@ fn test_check_merge_no_conflict() {
})), })),
}), }),
options: None, options: None,
}) }))
.expect("check_merge"); .await
.unwrap()
.into_inner();
assert!( assert!(
result.status == merge_result::Status::MergeResultStatusMerged as i32 result.status == merge_result::Status::MergeResultStatusMerged as i32
@@ -31,12 +44,13 @@ fn test_check_merge_no_conflict() {
); );
} }
#[test] #[tokio::test]
fn test_check_merge_with_conflict() { async fn test_check_merge_with_conflict() {
let (_dir, gb) = common::setup_bare_repo_with_conflict(); let (dir, _gb) = common::setup_bare_repo_with_conflict();
let result = gb let svc = common::setup_service(dir.path());
.check_merge(CheckMergeRequest { let result = svc
repository: None, .check_merge(tonic::Request::new(CheckMergeRequest {
repository: Some(hdr()),
target: Some(ObjectSelector { target: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "branch-a".into(), revision: "branch-a".into(),
@@ -48,8 +62,10 @@ fn test_check_merge_with_conflict() {
})), })),
}), }),
options: None, options: None,
}) }))
.expect("check_merge with conflict"); .await
.unwrap()
.into_inner();
assert_eq!( assert_eq!(
result.status, result.status,
@@ -59,12 +75,13 @@ fn test_check_merge_with_conflict() {
assert!(!result.conflicts.is_empty(), "should list conflicted files"); assert!(!result.conflicts.is_empty(), "should list conflicted files");
} }
#[test] #[tokio::test]
fn test_check_merge_already_up_to_date() { async fn test_check_merge_already_up_to_date() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let result = gb let svc = common::setup_service(dir.path());
.check_merge(CheckMergeRequest { let result = svc
repository: None, .check_merge(tonic::Request::new(CheckMergeRequest {
repository: Some(hdr()),
target: Some(ObjectSelector { target: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main".into(), revision: "main".into(),
@@ -76,8 +93,10 @@ fn test_check_merge_already_up_to_date() {
})), })),
}), }),
options: None, options: None,
}) }))
.expect("check_merge same ref"); .await
.unwrap()
.into_inner();
assert_eq!( assert_eq!(
result.status, result.status,
@@ -85,12 +104,13 @@ fn test_check_merge_already_up_to_date() {
); );
} }
#[test] #[tokio::test]
fn test_merge_fast_forward() { async fn test_merge_fast_forward() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let result = gb let svc = common::setup_service(dir.path());
.merge(MergeRequest { let result = svc
repository: None, .merge(tonic::Request::new(MergeRequest {
repository: Some(hdr()),
target_branch: "feature".into(), target_branch: "feature".into(),
source: Some(ObjectSelector { source: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
@@ -100,8 +120,10 @@ fn test_merge_fast_forward() {
committer: None, committer: None,
message: String::new(), message: String::new(),
options: None, options: None,
}) }))
.expect("merge fast-forward"); .await
.unwrap()
.into_inner();
assert!( assert!(
result.status == merge_result::Status::MergeResultStatusFastForward as i32 result.status == merge_result::Status::MergeResultStatusFastForward as i32
@@ -111,12 +133,13 @@ fn test_merge_fast_forward() {
); );
} }
#[test] #[tokio::test]
fn test_merge_with_conflict() { async fn test_merge_with_conflict() {
let (_dir, gb) = common::setup_bare_repo_with_conflict(); let (dir, _gb) = common::setup_bare_repo_with_conflict();
let result = gb let svc = common::setup_service(dir.path());
.merge(MergeRequest { let result = svc
repository: None, .merge(tonic::Request::new(MergeRequest {
repository: Some(hdr()),
target_branch: "branch-a".into(), target_branch: "branch-a".into(),
source: Some(ObjectSelector { source: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
@@ -126,8 +149,10 @@ fn test_merge_with_conflict() {
committer: None, committer: None,
message: String::new(), message: String::new(),
options: None, options: None,
}) }))
.expect("merge with conflict"); .await
.unwrap()
.into_inner();
assert_eq!( assert_eq!(
result.status, result.status,
@@ -136,12 +161,13 @@ fn test_merge_with_conflict() {
); );
} }
#[test] #[tokio::test]
fn test_merge_fast_forward_only_aborts_non_fast_forward() { async fn test_merge_fast_forward_only_aborts_non_fast_forward() {
let (_dir, gb) = common::setup_bare_repo_with_conflict(); let (dir, _gb) = common::setup_bare_repo_with_conflict();
let result = gb let svc = common::setup_service(dir.path());
.merge(MergeRequest { let result = svc
repository: None, .merge(tonic::Request::new(MergeRequest {
repository: Some(hdr()),
target_branch: "branch-a".into(), target_branch: "branch-a".into(),
source: Some(ObjectSelector { source: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
@@ -154,8 +180,10 @@ fn test_merge_fast_forward_only_aborts_non_fast_forward() {
fast_forward: merge_options::FastForwardMode::MergeFastForwardModeOnly as i32, fast_forward: merge_options::FastForwardMode::MergeFastForwardModeOnly as i32,
..Default::default() ..Default::default()
}), }),
}) }))
.expect("merge fast-forward only"); .await
.unwrap()
.into_inner();
assert_eq!( assert_eq!(
result.status, result.status,
@@ -164,12 +192,13 @@ fn test_merge_fast_forward_only_aborts_non_fast_forward() {
assert!(result.commit.is_none()); assert!(result.commit.is_none());
} }
#[test] #[tokio::test]
fn test_list_merge_conflicts() { async fn test_list_merge_conflicts() {
let (_dir, gb) = common::setup_bare_repo_with_conflict(); let (dir, _gb) = common::setup_bare_repo_with_conflict();
let result = gb let svc = common::setup_service(dir.path());
.list_merge_conflicts(ListMergeConflictsRequest { let result = svc
repository: None, .list_merge_conflicts(tonic::Request::new(ListMergeConflictsRequest {
repository: Some(hdr()),
target: Some(ObjectSelector { target: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "branch-a".into(), revision: "branch-a".into(),
@@ -181,8 +210,10 @@ fn test_list_merge_conflicts() {
})), })),
}), }),
pagination: None, pagination: None,
}) }))
.expect("list_merge_conflicts"); .await
.unwrap()
.into_inner();
assert!(!result.conflicts.is_empty(), "should list conflicted files"); assert!(!result.conflicts.is_empty(), "should list conflicted files");
assert!( assert!(
@@ -191,13 +222,14 @@ fn test_list_merge_conflicts() {
); );
} }
#[test] #[tokio::test]
fn test_resolve_merge_conflicts() { async fn test_resolve_merge_conflicts() {
let (_dir, gb) = common::setup_bare_repo_with_conflict(); let (dir, _gb) = common::setup_bare_repo_with_conflict();
let svc = common::setup_service(dir.path());
let result = gb let result = svc
.resolve_merge_conflicts(ResolveMergeConflictsRequest { .resolve_merge_conflicts(tonic::Request::new(ResolveMergeConflictsRequest {
repository: None, repository: Some(hdr()),
target_branch: "branch-a".into(), target_branch: "branch-a".into(),
source: Some(ObjectSelector { source: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
@@ -210,8 +242,10 @@ fn test_resolve_merge_conflicts() {
}], }],
committer: None, committer: None,
message: "resolved conflicts".into(), message: "resolved conflicts".into(),
}) }))
.expect("resolve_merge_conflicts"); .await
.unwrap()
.into_inner();
assert_eq!( assert_eq!(
result.status, result.status,
@@ -219,9 +253,9 @@ fn test_resolve_merge_conflicts() {
); );
assert!(result.commit.is_some()); assert!(result.commit.is_some());
let blob = gb let blob = svc
.get_blob(GetBlobRequest { .get_blob(tonic::Request::new(GetBlobRequest {
repository: None, repository: Some(hdr()),
revision: Some(ObjectSelector { revision: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "branch-a".into(), revision: "branch-a".into(),
@@ -230,17 +264,20 @@ fn test_resolve_merge_conflicts() {
path: "file.txt".into(), path: "file.txt".into(),
oid: None, oid: None,
max_bytes: 0, max_bytes: 0,
}) }))
.expect("get resolved blob"); .await
.unwrap()
.into_inner();
assert_eq!(String::from_utf8_lossy(&blob.data), "resolved content\n"); assert_eq!(String::from_utf8_lossy(&blob.data), "resolved content\n");
} }
#[test] #[tokio::test]
fn test_rebase() { async fn test_rebase() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let svc = common::setup_service(dir.path());
gb.create_commit(CreateCommitRequest { svc.create_commit(tonic::Request::new(CreateCommitRequest {
repository: None, repository: Some(hdr()),
branch: "feature".into(), branch: "feature".into(),
message: "feature work".into(), message: "feature work".into(),
author: None, author: None,
@@ -261,12 +298,13 @@ fn test_rebase() {
}), }),
force: false, force: false,
trailers: vec![], trailers: vec![],
}) }))
.expect("create feature commit"); .await
.unwrap();
let result = gb let result = svc
.rebase(RebaseRequest { .rebase(tonic::Request::new(RebaseRequest {
repository: None, repository: Some(hdr()),
branch: "feature".into(), branch: "feature".into(),
upstream: Some(ObjectSelector { upstream: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
@@ -274,8 +312,10 @@ fn test_rebase() {
})), })),
}), }),
committer: None, committer: None,
}) }))
.expect("rebase"); .await
.unwrap()
.into_inner();
assert_eq!( assert_eq!(
result.status, result.status,
@@ -283,9 +323,9 @@ fn test_rebase() {
); );
assert!(result.head.is_some()); assert!(result.head.is_some());
let blob = gb let blob = svc
.get_blob(GetBlobRequest { .get_blob(tonic::Request::new(GetBlobRequest {
repository: None, repository: Some(hdr()),
revision: Some(ObjectSelector { revision: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "feature".into(), revision: "feature".into(),
@@ -294,7 +334,9 @@ fn test_rebase() {
path: "feature.txt".into(), path: "feature.txt".into(),
oid: None, oid: None,
max_bytes: 0, max_bytes: 0,
}) }))
.expect("get rebased feature file"); .await
.unwrap()
.into_inner();
assert_eq!(String::from_utf8_lossy(&blob.data), "feature content"); assert_eq!(String::from_utf8_lossy(&blob.data), "feature content");
} }
+128 -35
View File
@@ -3,10 +3,7 @@ mod common;
use gitks::bare::GitBare; use gitks::bare::GitBare;
use gitks::pb::repository_service_server::RepositoryService; use gitks::pb::repository_service_server::RepositoryService;
use gitks::pb::*; use gitks::pb::*;
use gitks::server::GitksService;
fn header(gb: &GitBare) -> RepositoryHeader { fn header(gb: &GitBare) -> RepositoryHeader {
let parent = gb.bare_dir.parent().unwrap().to_string_lossy().into_owned();
let name = gb let name = gb
.bare_dir .bare_dir
.file_name() .file_name()
@@ -14,7 +11,6 @@ fn header(gb: &GitBare) -> RepositoryHeader {
.to_string_lossy() .to_string_lossy()
.into_owned(); .into_owned();
RepositoryHeader { RepositoryHeader {
storage_path: parent,
relative_path: name, relative_path: name,
..Default::default() ..Default::default()
} }
@@ -26,8 +22,9 @@ fn req<T>(gb: &GitBare, f: impl FnOnce(RepositoryHeader) -> T) -> tonic::Request
#[tokio::test] #[tokio::test]
async fn test_get_repository() { async fn test_get_repository() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, gb) = common::setup_bare_repo();
let repo = GitksService let svc = common::setup_service(dir.path());
let repo = svc
.get_repository(req(&gb, |r| GetRepositoryRequest { .get_repository(req(&gb, |r| GetRepositoryRequest {
repository: Some(r), repository: Some(r),
})) }))
@@ -42,13 +39,11 @@ async fn test_get_repository() {
#[tokio::test] #[tokio::test]
async fn test_init_and_delete_repository() { async fn test_init_and_delete_repository() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let storage = dir.path().to_string_lossy().into_owned(); let svc = common::setup_service(dir.path());
let hdr = RepositoryHeader { let hdr = RepositoryHeader {
storage_path: storage.clone(),
relative_path: "new-repo".into(), relative_path: "new-repo".into(),
..Default::default() ..Default::default()
}; };
let svc = GitksService;
svc.init_repository(tonic::Request::new(InitRepositoryRequest { svc.init_repository(tonic::Request::new(InitRepositoryRequest {
repository: Some(hdr.clone()), repository: Some(hdr.clone()),
bare: true, bare: true,
@@ -83,8 +78,9 @@ async fn test_init_and_delete_repository() {
#[tokio::test] #[tokio::test]
async fn test_get_object_format() { async fn test_get_object_format() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, gb) = common::setup_bare_repo();
let resp = GitksService let svc = common::setup_service(dir.path());
let resp = svc
.get_object_format(req(&gb, |r| RepositoryObjectFormatRequest { .get_object_format(req(&gb, |r| RepositoryObjectFormatRequest {
repository: Some(r), repository: Some(r),
})) }))
@@ -96,11 +92,11 @@ async fn test_get_object_format() {
#[tokio::test] #[tokio::test]
async fn test_get_set_default_branch() { async fn test_get_set_default_branch() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, gb) = common::setup_bare_repo();
let svc = common::setup_service(dir.path());
let h = header(&gb); let h = header(&gb);
assert_eq!( assert_eq!(
GitksService svc.get_default_branch(tonic::Request::new(GetDefaultBranchRequest {
.get_default_branch(tonic::Request::new(GetDefaultBranchRequest {
repository: Some(h.clone()) repository: Some(h.clone())
})) }))
.await .await
@@ -109,16 +105,14 @@ async fn test_get_set_default_branch() {
.name, .name,
"main" "main"
); );
GitksService svc.set_default_branch(tonic::Request::new(SetDefaultBranchRequest {
.set_default_branch(tonic::Request::new(SetDefaultBranchRequest {
repository: Some(h.clone()), repository: Some(h.clone()),
name: "feature".into(), name: "feature".into(),
})) }))
.await .await
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
GitksService svc.get_default_branch(tonic::Request::new(GetDefaultBranchRequest {
.get_default_branch(tonic::Request::new(GetDefaultBranchRequest {
repository: Some(h) repository: Some(h)
})) }))
.await .await
@@ -131,9 +125,9 @@ async fn test_get_set_default_branch() {
#[tokio::test] #[tokio::test]
async fn test_get_set_repository_config() { async fn test_get_set_repository_config() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, gb) = common::setup_bare_repo();
GitksService let svc = common::setup_service(dir.path());
.set_repository_config(tonic::Request::new(SetRepositoryConfigRequest { svc.set_repository_config(tonic::Request::new(SetRepositoryConfigRequest {
repository: Some(header(&gb)), repository: Some(header(&gb)),
entries: vec![RepositoryConfigEntry { entries: vec![RepositoryConfigEntry {
key: "test.key".into(), key: "test.key".into(),
@@ -142,7 +136,7 @@ async fn test_get_set_repository_config() {
})) }))
.await .await
.unwrap(); .unwrap();
let entry = GitksService let entry = svc
.get_repository_config(tonic::Request::new(GetRepositoryConfigRequest { .get_repository_config(tonic::Request::new(GetRepositoryConfigRequest {
repository: Some(header(&gb)), repository: Some(header(&gb)),
keys: vec!["test.key".into()], keys: vec!["test.key".into()],
@@ -159,8 +153,9 @@ async fn test_get_set_repository_config() {
#[tokio::test] #[tokio::test]
async fn test_get_repository_statistics() { async fn test_get_repository_statistics() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, gb) = common::setup_bare_repo();
let s = GitksService let svc = common::setup_service(dir.path());
let s = svc
.get_repository_statistics(req(&gb, |r| RepositoryStatisticsRequest { .get_repository_statistics(req(&gb, |r| RepositoryStatisticsRequest {
repository: Some(r), repository: Some(r),
})) }))
@@ -174,8 +169,9 @@ async fn test_get_repository_statistics() {
#[tokio::test] #[tokio::test]
async fn test_check_repository_health() { async fn test_check_repository_health() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, gb) = common::setup_bare_repo();
let h = GitksService let svc = common::setup_service(dir.path());
let h = svc
.check_repository_health(tonic::Request::new(RepositoryHealthRequest { .check_repository_health(tonic::Request::new(RepositoryHealthRequest {
repository: Some(header(&gb)), repository: Some(header(&gb)),
connectivity_only: true, connectivity_only: true,
@@ -188,10 +184,10 @@ async fn test_check_repository_health() {
#[tokio::test] #[tokio::test]
async fn test_garbage_collect() { async fn test_garbage_collect() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, gb) = common::setup_bare_repo();
let svc = common::setup_service(dir.path());
assert!( assert!(
GitksService svc.garbage_collect(tonic::Request::new(GarbageCollectRequest {
.garbage_collect(tonic::Request::new(GarbageCollectRequest {
repository: Some(header(&gb)), repository: Some(header(&gb)),
..Default::default() ..Default::default()
})) }))
@@ -204,10 +200,10 @@ async fn test_garbage_collect() {
#[tokio::test] #[tokio::test]
async fn test_repack() { async fn test_repack() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, gb) = common::setup_bare_repo();
let svc = common::setup_service(dir.path());
assert!( assert!(
GitksService svc.repack(tonic::Request::new(RepackRequest {
.repack(tonic::Request::new(RepackRequest {
repository: Some(header(&gb)), repository: Some(header(&gb)),
..Default::default() ..Default::default()
})) }))
@@ -220,10 +216,10 @@ async fn test_repack() {
#[tokio::test] #[tokio::test]
async fn test_write_commit_graph() { async fn test_write_commit_graph() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, gb) = common::setup_bare_repo();
let svc = common::setup_service(dir.path());
assert!( assert!(
GitksService svc.write_commit_graph(tonic::Request::new(WriteCommitGraphRequest {
.write_commit_graph(tonic::Request::new(WriteCommitGraphRequest {
repository: Some(header(&gb)), repository: Some(header(&gb)),
..Default::default() ..Default::default()
})) }))
@@ -233,3 +229,100 @@ async fn test_write_commit_graph() {
.ok .ok
); );
} }
#[tokio::test]
async fn test_resolve_none_header() {
let dir = tempfile::tempdir().unwrap();
let svc = common::setup_service(dir.path());
let result = svc
.get_repository(tonic::Request::new(GetRepositoryRequest {
repository: None,
}))
.await;
assert!(result.is_err(), "should fail with None header");
let err = result.unwrap_err();
assert_eq!(err.code(), tonic::Code::InvalidArgument);
}
#[tokio::test]
async fn test_resolve_empty_relative_path() {
let dir = tempfile::tempdir().unwrap();
let svc = common::setup_service(dir.path());
let result = svc
.get_repository(tonic::Request::new(GetRepositoryRequest {
repository: Some(RepositoryHeader {
relative_path: String::new(),
..Default::default()
}),
}))
.await;
assert!(result.is_err(), "should fail with empty relative_path");
}
#[tokio::test]
async fn test_resolve_nonexistent_repo() {
let dir = tempfile::tempdir().unwrap();
let svc = common::setup_service(dir.path());
let result = svc
.get_repository(tonic::Request::new(GetRepositoryRequest {
repository: Some(RepositoryHeader {
relative_path: "does-not-exist".into(),
..Default::default()
}),
}))
.await;
assert!(result.is_err(), "should fail for nonexistent repo");
}
#[tokio::test]
async fn test_init_empty_relative_path() {
let dir = tempfile::tempdir().unwrap();
let svc = common::setup_service(dir.path());
let result = svc
.init_repository(tonic::Request::new(InitRepositoryRequest {
repository: Some(RepositoryHeader {
relative_path: String::new(),
..Default::default()
}),
bare: true,
..Default::default()
}))
.await;
assert!(result.is_err(), "should fail with empty relative_path");
}
#[tokio::test]
async fn test_delete_nonexistent_repo() {
let dir = tempfile::tempdir().unwrap();
let svc = common::setup_service(dir.path());
// Deleting a non-existent path should succeed (fs::remove_dir_all on non-existent is ok)
// or fail gracefully
let result = svc
.delete_repository(tonic::Request::new(DeleteRepositoryRequest {
repository: Some(RepositoryHeader {
relative_path: "ghost-repo".into(),
..Default::default()
}),
}))
.await;
// It either succeeds (dir doesn't exist, nothing to delete) or fails
// Both are acceptable behaviors
let _ = result;
}
#[tokio::test]
async fn test_exists_nonexistent_repo() {
let dir = tempfile::tempdir().unwrap();
let svc = common::setup_service(dir.path());
let result = svc
.repository_exists(tonic::Request::new(RepositoryExistsRequest {
repository: Some(RepositoryHeader {
relative_path: "nonexistent".into(),
..Default::default()
}),
}))
.await
.unwrap()
.into_inner();
assert!(!result.exists);
}
+89 -59
View File
@@ -1,44 +1,59 @@
mod common; mod common;
use gitks::pb::tag_service_server::TagService;
use gitks::pb::*; use gitks::pb::*;
#[test] fn hdr() -> RepositoryHeader {
fn test_list_tags() { RepositoryHeader {
let (_dir, gb) = common::setup_bare_repo(); relative_path: "test-repo".into(),
let result = gb ..Default::default()
.list_tags(ListTagsRequest { }
repository: None, }
#[tokio::test]
async fn test_list_tags() {
let (dir, _gb) = common::setup_bare_repo();
let svc = common::setup_service(dir.path());
let result = svc
.list_tags(tonic::Request::new(ListTagsRequest {
repository: Some(hdr()),
pattern: String::new(), pattern: String::new(),
pagination: None, pagination: None,
sort_direction: 0, sort_direction: 0,
}) }))
.expect("list_tags"); .await
.unwrap()
.into_inner();
let names: Vec<String> = result.tags.iter().map(|t| t.name.clone()).collect(); let names: Vec<String> = result.tags.iter().map(|t| t.name.clone()).collect();
assert!(names.contains(&"v0.1.0".to_string())); assert!(names.contains(&"v0.1.0".to_string()));
} }
#[test] #[tokio::test]
fn test_get_tag() { async fn test_get_tag() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let tag = gb let svc = common::setup_service(dir.path());
.get_tag(GetTagRequest { let tag = svc
repository: None, .get_tag(tonic::Request::new(GetTagRequest {
repository: Some(hdr()),
name: "v0.1.0".into(), name: "v0.1.0".into(),
include_raw: false, include_raw: false,
}) }))
.expect("get_tag"); .await
.unwrap()
.into_inner();
assert_eq!(tag.name, "v0.1.0"); assert_eq!(tag.name, "v0.1.0");
assert!(tag.target_oid.is_some()); assert!(tag.target_oid.is_some());
assert_eq!(tag.full_ref, "refs/tags/v0.1.0"); assert_eq!(tag.full_ref, "refs/tags/v0.1.0");
} }
#[test] #[tokio::test]
fn test_create_and_delete_lightweight_tag() { async fn test_create_and_delete_lightweight_tag() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let tag = gb let svc = common::setup_service(dir.path());
.create_tag(CreateTagRequest { let tag = svc
repository: None, .create_tag(tonic::Request::new(CreateTagRequest {
repository: Some(hdr()),
name: "v0.2.0".into(), name: "v0.2.0".into(),
target: Some(ObjectSelector { target: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
@@ -49,32 +64,38 @@ fn test_create_and_delete_lightweight_tag() {
tagger: None, tagger: None,
force: false, force: false,
annotated: false, annotated: false,
}) }))
.expect("create_tag"); .await
.unwrap()
.into_inner();
assert_eq!(tag.name, "v0.2.0"); assert_eq!(tag.name, "v0.2.0");
assert!(!tag.annotated); assert!(!tag.annotated);
gb.delete_tag(DeleteTagRequest { svc.delete_tag(tonic::Request::new(DeleteTagRequest {
repository: None, repository: Some(hdr()),
name: "v0.2.0".into(), name: "v0.2.0".into(),
}) }))
.expect("delete_tag"); .await
.unwrap();
let result = gb.get_tag(GetTagRequest { let result = svc
repository: None, .get_tag(tonic::Request::new(GetTagRequest {
repository: Some(hdr()),
name: "v0.2.0".into(), name: "v0.2.0".into(),
include_raw: false, include_raw: false,
}); }))
.await;
assert!(result.is_err(), "deleted tag should not exist"); assert!(result.is_err(), "deleted tag should not exist");
} }
#[test] #[tokio::test]
fn test_create_annotated_tag() { async fn test_create_annotated_tag() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let tag = gb let svc = common::setup_service(dir.path());
.create_tag(CreateTagRequest { let tag = svc
repository: None, .create_tag(tonic::Request::new(CreateTagRequest {
repository: Some(hdr()),
name: "v1.0.0".into(), name: "v1.0.0".into(),
target: Some(ObjectSelector { target: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
@@ -85,8 +106,10 @@ fn test_create_annotated_tag() {
tagger: None, tagger: None,
force: false, force: false,
annotated: true, annotated: true,
}) }))
.expect("create annotated tag"); .await
.unwrap()
.into_inner();
assert_eq!(tag.name, "v1.0.0"); assert_eq!(tag.name, "v1.0.0");
assert!(tag.annotated, "should be annotated"); assert!(tag.annotated, "should be annotated");
@@ -97,12 +120,13 @@ fn test_create_annotated_tag() {
); );
} }
#[test] #[tokio::test]
fn test_list_tags_with_pattern() { async fn test_list_tags_with_pattern() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let svc = common::setup_service(dir.path());
gb.create_tag(CreateTagRequest { svc.create_tag(tonic::Request::new(CreateTagRequest {
repository: None, repository: Some(hdr()),
name: "release-1.0".into(), name: "release-1.0".into(),
target: Some(ObjectSelector { target: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
@@ -113,17 +137,20 @@ fn test_list_tags_with_pattern() {
tagger: None, tagger: None,
force: false, force: false,
annotated: false, annotated: false,
}) }))
.expect("create release tag"); .await
.unwrap();
let result = gb let result = svc
.list_tags(ListTagsRequest { .list_tags(tonic::Request::new(ListTagsRequest {
repository: None, repository: Some(hdr()),
pattern: "release".into(), pattern: "release".into(),
pagination: None, pagination: None,
sort_direction: 0, sort_direction: 0,
}) }))
.expect("list_tags with pattern"); .await
.unwrap()
.into_inner();
assert!( assert!(
result.tags.iter().all(|t| t.name.contains("release")), result.tags.iter().all(|t| t.name.contains("release")),
@@ -132,15 +159,18 @@ fn test_list_tags_with_pattern() {
assert!(!result.tags.is_empty()); assert!(!result.tags.is_empty());
} }
#[test] #[tokio::test]
fn test_verify_tag() { async fn test_verify_tag() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let result = gb let svc = common::setup_service(dir.path());
.verify_tag(VerifyTagRequest { let result = svc
repository: None, .verify_tag(tonic::Request::new(VerifyTagRequest {
repository: Some(hdr()),
name: "v0.1.0".into(), name: "v0.1.0".into(),
}) }))
.expect("verify_tag"); .await
.unwrap()
.into_inner();
assert!(!result.verified, "unsigned tag should not be verified"); assert!(!result.verified, "unsigned tag should not be verified");
} }
+119 -61
View File
@@ -1,13 +1,22 @@
mod common; mod common;
use gitks::pb::tree_service_server::TreeService;
use gitks::pb::*; use gitks::pb::*;
#[test] fn hdr() -> RepositoryHeader {
fn test_list_tree_recursive() { RepositoryHeader {
let (_dir, gb) = common::setup_bare_repo(); relative_path: "test-repo".into(),
let result = gb ..Default::default()
.list_tree(ListTreeRequest { }
repository: None, }
#[tokio::test]
async fn test_list_tree_recursive() {
let (dir, _gb) = common::setup_bare_repo();
let svc = common::setup_service(dir.path());
let result = svc
.list_tree(tonic::Request::new(ListTreeRequest {
repository: Some(hdr()),
revision: Some(ObjectSelector { revision: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main".into(), revision: "main".into(),
@@ -16,8 +25,10 @@ fn test_list_tree_recursive() {
path: String::new(), path: String::new(),
recursive: true, recursive: true,
pagination: None, pagination: None,
}) }))
.expect("list_tree recursive"); .await
.unwrap()
.into_inner();
let paths: Vec<String> = result.entries.iter().map(|e| e.path.clone()).collect(); let paths: Vec<String> = result.entries.iter().map(|e| e.path.clone()).collect();
assert!( assert!(
@@ -27,33 +38,38 @@ fn test_list_tree_recursive() {
); );
} }
#[test] #[tokio::test]
fn test_get_tree_subpath() { async fn test_get_tree_subpath() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let result = gb let svc = common::setup_service(dir.path());
.get_tree(GetTreeRequest { let result = svc
repository: None, .get_tree(tonic::Request::new(GetTreeRequest {
repository: Some(hdr()),
revision: Some(ObjectSelector { revision: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main".into(), revision: "main".into(),
})), })),
}), }),
path: "src".into(), path: "src".into(),
}) }))
.expect("get_tree subpath"); .await
.unwrap()
.into_inner();
assert!(result.oid.is_some()); assert!(result.oid.is_some());
let root_tree = gb let root_tree = svc
.get_tree(GetTreeRequest { .get_tree(tonic::Request::new(GetTreeRequest {
repository: None, repository: Some(hdr()),
revision: Some(ObjectSelector { revision: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main".into(), revision: "main".into(),
})), })),
}), }),
path: String::new(), path: String::new(),
}) }))
.expect("get_tree root"); .await
.unwrap()
.into_inner();
assert_ne!( assert_ne!(
result.oid.unwrap().hex, result.oid.unwrap().hex,
root_tree.oid.unwrap().hex, root_tree.oid.unwrap().hex,
@@ -61,12 +77,13 @@ fn test_get_tree_subpath() {
); );
} }
#[test] #[tokio::test]
fn test_find_files() { async fn test_find_files() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let result = gb let svc = common::setup_service(dir.path());
.find_files(FindFilesRequest { let result = svc
repository: None, .find_files(tonic::Request::new(FindFilesRequest {
repository: Some(hdr()),
revision: Some(ObjectSelector { revision: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main".into(), revision: "main".into(),
@@ -75,19 +92,22 @@ fn test_find_files() {
pattern: "mod.rs".into(), pattern: "mod.rs".into(),
pathspec: vec![], pathspec: vec![],
pagination: None, pagination: None,
}) }))
.expect("find_files"); .await
.unwrap()
.into_inner();
assert!(!result.files.is_empty()); assert!(!result.files.is_empty());
assert!(result.files.iter().all(|f| f.path.contains("mod.rs"))); assert!(result.files.iter().all(|f| f.path.contains("mod.rs")));
} }
#[test] #[tokio::test]
fn test_get_blob() { async fn test_get_blob() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let blob = gb let svc = common::setup_service(dir.path());
.get_blob(GetBlobRequest { let blob = svc
repository: None, .get_blob(tonic::Request::new(GetBlobRequest {
repository: Some(hdr()),
revision: Some(ObjectSelector { revision: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main".into(), revision: "main".into(),
@@ -96,8 +116,10 @@ fn test_get_blob() {
path: "README.md".into(), path: "README.md".into(),
oid: None, oid: None,
max_bytes: 0, max_bytes: 0,
}) }))
.expect("get_blob"); .await
.unwrap()
.into_inner();
let content = String::from_utf8_lossy(&blob.data); let content = String::from_utf8_lossy(&blob.data);
assert!(content.contains("# Test")); assert!(content.contains("# Test"));
@@ -105,12 +127,13 @@ fn test_get_blob() {
assert!(!blob.binary); assert!(!blob.binary);
} }
#[test] #[tokio::test]
fn test_get_blob_with_truncation() { async fn test_get_blob_with_truncation() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let blob = gb let svc = common::setup_service(dir.path());
.get_blob(GetBlobRequest { let blob = svc
repository: None, .get_blob(tonic::Request::new(GetBlobRequest {
repository: Some(hdr()),
revision: Some(ObjectSelector { revision: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main".into(), revision: "main".into(),
@@ -119,8 +142,10 @@ fn test_get_blob_with_truncation() {
path: "README.md".into(), path: "README.md".into(),
oid: None, oid: None,
max_bytes: 5, max_bytes: 5,
}) }))
.expect("get_blob truncated"); .await
.unwrap()
.into_inner();
assert_eq!(blob.data.len(), 5); assert_eq!(blob.data.len(), 5);
assert!(blob.truncated); assert!(blob.truncated);
@@ -130,32 +155,36 @@ fn test_get_blob_with_truncation() {
); );
} }
#[test] #[tokio::test]
fn test_get_file_metadata() { async fn test_get_file_metadata() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let meta = gb let svc = common::setup_service(dir.path());
.get_file_metadata(GetFileMetadataRequest { let meta = svc
repository: None, .get_file_metadata(tonic::Request::new(GetFileMetadataRequest {
repository: Some(hdr()),
revision: Some(ObjectSelector { revision: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main".into(), revision: "main".into(),
})), })),
}), }),
path: "README.md".into(), path: "README.md".into(),
}) }))
.expect("get_file_metadata"); .await
.unwrap()
.into_inner();
assert_eq!(meta.path, "README.md"); assert_eq!(meta.path, "README.md");
assert!(meta.oid.is_some()); assert!(meta.oid.is_some());
assert_eq!(meta.r#type, ObjectType::Blob as i32); assert_eq!(meta.r#type, ObjectType::Blob as i32);
} }
#[test] #[tokio::test]
fn test_list_tree_with_pagination() { async fn test_list_tree_with_pagination() {
let (_dir, gb) = common::setup_bare_repo(); let (dir, _gb) = common::setup_bare_repo();
let result = gb let svc = common::setup_service(dir.path());
.list_tree(ListTreeRequest { let result = svc
repository: None, .list_tree(tonic::Request::new(ListTreeRequest {
repository: Some(hdr()),
revision: Some(ObjectSelector { revision: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName { selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main".into(), revision: "main".into(),
@@ -167,10 +196,39 @@ fn test_list_tree_with_pagination() {
page_size: 1, page_size: 1,
page_token: String::new(), page_token: String::new(),
}), }),
}) }))
.expect("list_tree paginated"); .await
.unwrap()
.into_inner();
assert_eq!(result.entries.len(), 1); assert_eq!(result.entries.len(), 1);
let pi = result.page_info.unwrap(); let pi = result.page_info.unwrap();
assert!(pi.has_next_page); assert!(pi.has_next_page);
} }
#[tokio::test]
async fn test_get_raw_blob() {
let (dir, _gb) = common::setup_bare_repo();
let svc = common::setup_service(dir.path());
let stream = svc
.get_raw_blob(tonic::Request::new(GetRawBlobRequest {
repository: Some(hdr()),
revision: Some(ObjectSelector {
selector: Some(object_selector::Selector::Revision(ObjectName {
revision: "main".into(),
})),
}),
path: "README.md".into(),
oid: None,
}))
.await
.unwrap()
.into_inner();
let chunks: Vec<_> = tokio_stream::StreamExt::collect(stream).await;
assert!(!chunks.is_empty(), "should have raw blob data");
let data = &chunks[0].as_ref().unwrap().data;
assert!(!data.is_empty(), "raw blob should not be empty");
let content = String::from_utf8_lossy(data);
assert!(content.contains("# Test"));
}
+4 -6
View File
@@ -4,12 +4,10 @@ use crate::paginate;
use crate::pb::{ use crate::pb::{
FileMetadata, FindFilesRequest, FindFilesResponse, ListTreeRequest, ObjectType, tree_entry, FileMetadata, FindFilesRequest, FindFilesResponse, ListTreeRequest, ObjectType, tree_entry,
}; };
use crate::tree;
impl GitBare { impl GitBare {
pub fn find_files(&self, request: FindFilesRequest) -> GitResult<FindFilesResponse> { pub fn find_files(&self, request: FindFilesRequest) -> GitResult<FindFilesResponse> {
let revision = request.revision.clone(); let revision = request.revision.clone();
let rev = tree::resolve_revision(&revision);
let root = if request.pathspec.is_empty() { let root = if request.pathspec.is_empty() {
vec![String::new()] vec![String::new()]
} else { } else {
@@ -37,17 +35,17 @@ impl GitBare {
tree_entry::EntryType::TreeEntryTypeUnspecified => ObjectType::Unspecified, tree_entry::EntryType::TreeEntryTypeUnspecified => ObjectType::Unspecified,
_ => ObjectType::Blob, _ => ObjectType::Blob,
} as i32; } as i32;
let entry_path = entry.path.clone(); // recent_commit is NOT computed here to avoid N subprocess calls.
let rc = tree::recent_commit(self, &rev, &entry_path); // Use get_file_metadata for per-file commit info.
files.push(FileMetadata { files.push(FileMetadata {
path: entry_path, path: entry.path,
oid: entry.oid, oid: entry.oid,
mode: entry.mode, mode: entry.mode,
size: entry.size, size: entry.size,
r#type: object_type, r#type: object_type,
binary: false, binary: false,
is_lfs: false, is_lfs: false,
recent_commit: rc, recent_commit: None,
}); });
} }
} }
+4 -5
View File
@@ -4,7 +4,6 @@ use crate::bare::GitBare;
use crate::error::{GitError, GitResult}; use crate::error::{GitError, GitResult};
use crate::paginate; use crate::paginate;
use crate::pb::{ListTreeRequest, ListTreeResponse, TreeEntry, object_selector, tree_entry}; use crate::pb::{ListTreeRequest, ListTreeResponse, TreeEntry, object_selector, tree_entry};
use crate::tree;
impl GitBare { impl GitBare {
pub fn list_tree(&self, request: ListTreeRequest) -> GitResult<ListTreeResponse> { pub fn list_tree(&self, request: ListTreeRequest) -> GitResult<ListTreeResponse> {
@@ -41,23 +40,23 @@ impl GitBare {
}; };
let kind = entry.kind(); let kind = entry.kind();
let hex = entry.id().to_string(); let hex = entry.id().to_string();
let entry_path = path.clone();
entries.push(TreeEntry { entries.push(TreeEntry {
name, name,
path: entry_path.clone(), path,
oid: Some(self.oid_to_pb(hex)), oid: Some(self.oid_to_pb(hex)),
r#type: entry_type(kind) as i32, r#type: entry_type(kind) as i32,
mode: u32::from_str_radix(&format!("{:o}", entry.mode()), 8).unwrap_or(0), mode: u32::from_str_radix(&format!("{:o}", entry.mode()), 8).unwrap_or(0),
size: entry_size(&repo, entry.id().to_string().as_str()).unwrap_or(0), size: entry_size(&repo, entry.id().to_string().as_str()).unwrap_or(0),
is_lfs: false, is_lfs: false,
recent_commit: tree::recent_commit(self, &revision, &entry_path), recent_commit: None, // populated on demand, not per-entry subprocess
}); });
if request.recursive && matches!(kind, EntryKind::Tree) { if request.recursive && matches!(kind, EntryKind::Tree) {
let child_path = entries.last().unwrap().path.clone();
let child = self.list_tree(ListTreeRequest { let child = self.list_tree(ListTreeRequest {
repository: request.repository.clone(), repository: request.repository.clone(),
revision: request.revision.clone(), revision: request.revision.clone(),
path, path: child_path,
recursive: true, recursive: true,
pagination: None, pagination: None,
})?; })?;