diff --git a/Cargo.toml b/Cargo.toml index b433ef4..e0de082 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ gix = { version = "0.84.0", default-features = false, features = ["serde", "blam gix-archive = { version = "0.33.0", features = ["sha256","sha1","document-features"] } duct = { version = "1.1.1", features = [] } tracing = { version = "0.1.32", features = ["log"] } -tokio = { version = "1", features = ["rt-multi-thread", "macros", "process", "io-util", "sync"] } +tokio = { version = "1", features = ["rt-multi-thread", "macros", "process", "io-util", "sync", "net"] } tokio-stream = { version = "0.1.18", features = ["full"] } thiserror = { version = "2.0.18", features = [] } prost = "0.13" diff --git a/blob/get_blob.rs b/blob/get_blob.rs index d2e39c5..6e38072 100644 --- a/blob/get_blob.rs +++ b/blob/get_blob.rs @@ -2,11 +2,13 @@ use gix::object::tree::EntryKind; use crate::bare::GitBare; use crate::error::{GitError, GitResult}; -use crate::pb::{Blob, GetBlobRequest, object_selector}; +use crate::pb::{Blob, GetBlobRequest}; +use crate::tree; impl GitBare { pub fn get_blob(&self, request: GetBlobRequest) -> GitResult { let repo = self.gix_repo()?; + let revision = tree::resolve_revision(&request.revision); let (blob, mode, path) = if let Some(oid) = request.oid.as_ref() { let id = gix::hash::ObjectId::from_hex(oid.hex.as_bytes()) .map_err(|e| GitError::InvalidOid(e.to_string()))?; @@ -18,11 +20,6 @@ impl GitBare { request.path, ) } else { - let revision = match request.revision.and_then(|s| s.selector) { - Some(object_selector::Selector::Oid(oid)) => oid.hex, - Some(object_selector::Selector::Revision(name)) => name.revision, - None => "HEAD".into(), - }; let tree = repo .rev_parse_single(format!("{}^{{tree}}", revision).as_str())? .object()? @@ -56,6 +53,8 @@ impl GitBare { data.truncate(request.max_bytes as usize); } let hex = blob.id.to_string(); + let lfs = tree::is_lfs_pointer(&data); + let rc = tree::recent_commit(self, &revision, &path); Ok(Blob { oid: Some(self.oid_to_pb(hex)), path, @@ -65,6 +64,8 @@ impl GitBare { encoding: String::new(), truncated, data, + is_lfs: lfs, + recent_commit: rc, }) } } diff --git a/lib.rs b/lib.rs index e927730..fc1a004 100644 --- a/lib.rs +++ b/lib.rs @@ -13,5 +13,6 @@ pub mod pack; pub mod paginate; pub mod pb; pub mod refs; +pub mod server; pub mod tag; pub mod tree; diff --git a/proto/tree.proto b/proto/tree.proto index 848d1ca..a8db5f5 100644 --- a/proto/tree.proto +++ b/proto/tree.proto @@ -5,6 +5,12 @@ package gitks; import "oid.proto"; import "repository.proto"; +message RecentCommit { + Oid oid = 1; + string subject = 2; + int64 committed_timestamp = 3; +} + message TreeEntry { enum EntryType { TREE_ENTRY_TYPE_UNSPECIFIED = 0; @@ -21,6 +27,8 @@ message TreeEntry { EntryType type = 4; uint32 mode = 5; int64 size = 6; + bool is_lfs = 7; + RecentCommit recent_commit = 8; } message Tree { @@ -39,6 +47,8 @@ message Blob { string encoding = 6; bool binary = 7; bool truncated = 8; + bool is_lfs = 9; + RecentCommit recent_commit = 10; } message FileMetadata { @@ -48,6 +58,8 @@ message FileMetadata { int64 size = 4; ObjectType type = 5; bool binary = 6; + bool is_lfs = 7; + RecentCommit recent_commit = 8; } message ListTreeRequest { diff --git a/tree/find_files.rs b/tree/find_files.rs index c82f354..1a67f22 100644 --- a/tree/find_files.rs +++ b/tree/find_files.rs @@ -4,10 +4,12 @@ use crate::paginate; use crate::pb::{ FileMetadata, FindFilesRequest, FindFilesResponse, ListTreeRequest, ObjectType, tree_entry, }; +use crate::tree; impl GitBare { pub fn find_files(&self, request: FindFilesRequest) -> GitResult { let revision = request.revision.clone(); + let rev = tree::resolve_revision(&revision); let root = if request.pathspec.is_empty() { vec![String::new()] } else { @@ -35,13 +37,17 @@ impl GitBare { tree_entry::EntryType::TreeEntryTypeUnspecified => ObjectType::Unspecified, _ => ObjectType::Blob, } as i32; + let entry_path = entry.path.clone(); + let rc = tree::recent_commit(self, &rev, &entry_path); files.push(FileMetadata { - path: entry.path, + path: entry_path, oid: entry.oid, mode: entry.mode, size: entry.size, r#type: object_type, binary: false, + is_lfs: false, + recent_commit: rc, }); } } diff --git a/tree/get_file_metadata.rs b/tree/get_file_metadata.rs index abd74dc..c15d761 100644 --- a/tree/get_file_metadata.rs +++ b/tree/get_file_metadata.rs @@ -3,6 +3,7 @@ use gix::object::tree::EntryKind; use crate::bare::GitBare; use crate::error::{GitError, GitResult}; use crate::pb::{FileMetadata, GetFileMetadataRequest, ObjectType, object_selector}; +use crate::tree; impl GitBare { pub fn get_file_metadata(&self, request: GetFileMetadataRequest) -> GitResult { @@ -26,6 +27,7 @@ impl GitBare { EntryKind::Commit => ObjectType::Commit, _ => ObjectType::Blob, } as i32; + let rc = tree::recent_commit(self, &revision, &request.path); Ok(FileMetadata { path: request.path, oid: Some(self.oid_to_pb(hex)), @@ -33,6 +35,8 @@ impl GitBare { size: 0, r#type: kind, binary: false, + is_lfs: false, + recent_commit: rc, }) } } diff --git a/tree/list_tree.rs b/tree/list_tree.rs index 46b449d..bb5511e 100644 --- a/tree/list_tree.rs +++ b/tree/list_tree.rs @@ -4,6 +4,7 @@ use crate::bare::GitBare; use crate::error::{GitError, GitResult}; use crate::paginate; use crate::pb::{ListTreeRequest, ListTreeResponse, TreeEntry, object_selector, tree_entry}; +use crate::tree; impl GitBare { pub fn list_tree(&self, request: ListTreeRequest) -> GitResult { @@ -40,13 +41,16 @@ impl GitBare { }; let kind = entry.kind(); let hex = entry.id().to_string(); + let entry_path = path.clone(); entries.push(TreeEntry { name, - path: path.clone(), + path: entry_path.clone(), oid: Some(self.oid_to_pb(hex)), r#type: entry_type(kind) as i32, 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), + is_lfs: false, + recent_commit: tree::recent_commit(self, &revision, &entry_path), }); if request.recursive && matches!(kind, EntryKind::Tree) { diff --git a/tree/mod.rs b/tree/mod.rs index 7dbe0de..e724976 100644 --- a/tree/mod.rs +++ b/tree/mod.rs @@ -2,3 +2,49 @@ pub mod find_files; pub mod get_file_metadata; pub mod get_tree; pub mod list_tree; + +use crate::bare::GitBare; +use crate::pb::{self, RecentCommit, object_selector}; + +pub(crate) fn resolve_revision(sel: &Option) -> String { + match sel.as_ref().and_then(|s| s.selector.as_ref()) { + Some(object_selector::Selector::Oid(oid)) => oid.hex.clone(), + Some(object_selector::Selector::Revision(name)) => name.revision.clone(), + None => "HEAD".into(), + } +} + +pub(crate) fn recent_commit(gb: &GitBare, revision: &str, path: &str) -> Option { + let output = std::process::Command::new("git") + .args([ + "--git-dir", + &gb.bare_dir.to_string_lossy(), + "log", + "-1", + "--format=%H %s %at", + revision, + "--", + path, + ]) + .output() + .ok()?; + if !output.status.success() { + return None; + } + let line = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if line.is_empty() { + return None; + } + let (hex, rest) = line.split_once(' ')?; + let (subject, ts_str) = rest.rsplit_once(' ')?; + let ts: i64 = ts_str.parse().ok()?; + Some(RecentCommit { + oid: Some(gb.oid_to_pb(hex)), + subject: subject.to_string(), + committed_timestamp: ts, + }) +} + +pub(crate) fn is_lfs_pointer(data: &[u8]) -> bool { + data.starts_with(b"version https://git-lfs.github.com/spec/v1") +}