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:
Generated
+1
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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");
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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("@@"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||||
})?;
|
})?;
|
||||||
|
|||||||
Reference in New Issue
Block a user