gitdataai/libs/git/lfs/ops.rs
2026-04-15 09:08:09 +08:00

321 lines
11 KiB
Rust

//! LFS operations on a Git repository.
use std::fs;
use std::path::{Path, PathBuf};
use globset::Glob;
use crate::commit::types::CommitOid;
use crate::lfs::types::{LfsConfig, LfsEntry, LfsOid, LfsPointer};
use crate::{GitDomain, GitError, GitResult};
impl GitDomain {
pub fn lfs_pointer_from_blob(&self, oid: &CommitOid) -> GitResult<Option<LfsPointer>> {
let content = self.blob_content(oid)?;
Ok(LfsPointer::from_bytes(&content.content))
}
pub fn lfs_is_pointer(&self, oid: &CommitOid) -> GitResult<bool> {
let content = self.blob_content(oid)?;
Ok(LfsPointer::from_bytes(&content.content).is_some())
}
pub fn lfs_resolve_oid(&self, oid: &CommitOid) -> GitResult<Option<LfsOid>> {
let pointer = self.lfs_pointer_from_blob(oid)?;
Ok(pointer.map(|p| p.oid))
}
pub fn lfs_create_pointer(
&self,
oid: &LfsOid,
size: u64,
extra: &[(String, String)],
) -> GitResult<CommitOid> {
let pointer = LfsPointer {
version: "https://git-lfs.github.com/spec/v1".to_string(),
oid: oid.clone(),
size,
extra: extra.to_vec(),
};
self.blob_create_from_string(&pointer.to_string())
}
pub fn lfs_scan_tree(&self, tree_oid: &CommitOid, recursive: bool) -> GitResult<Vec<LfsEntry>> {
let tree_oid = tree_oid
.to_oid()
.map_err(|_| GitError::InvalidOid(tree_oid.to_string()))?;
let tree = self
.repo()
.find_tree(tree_oid)
.map_err(|_| GitError::ObjectNotFound(tree_oid.to_string()))?;
let mut entries = Vec::new();
self.lfs_scan_tree_impl(&mut entries, &tree, "", recursive)?;
Ok(entries)
}
fn lfs_scan_tree_impl(
&self,
out: &mut Vec<LfsEntry>,
tree: &git2::Tree<'_>,
prefix: &str,
recursive: bool,
) -> GitResult<()> {
for entry in tree.iter() {
let name = entry.name().unwrap_or("");
let full_path = if prefix.is_empty() {
name.to_string()
} else {
format!("{}/{}", prefix, name)
};
let blob_oid = entry.id();
let obj = match self.repo().find_object(blob_oid, None) {
Ok(o) => o,
Err(_) => continue,
};
if obj.kind() == Some(git2::ObjectType::Tree) {
if recursive {
let sub_tree = self
.repo()
.find_tree(blob_oid)
.map_err(|e| GitError::Internal(e.to_string()))?;
self.lfs_scan_tree_impl(out, &sub_tree, &full_path, recursive)?;
}
} else if let Some(blob) = obj.as_blob() {
if let Some(pointer) = LfsPointer::from_bytes(blob.content()) {
out.push(LfsEntry {
path: full_path,
pointer,
size: 0,
});
}
}
}
Ok(())
}
fn gitattributes_path(&self) -> PathBuf {
self.repo()
.workdir()
.unwrap_or_else(|| self.repo().path())
.join(".gitattributes")
}
pub fn lfs_gitattributes_list(&self) -> GitResult<Vec<String>> {
let path = self.gitattributes_path();
if !path.exists() {
return Ok(Vec::new());
}
let content = fs::read_to_string(&path).map_err(|e| GitError::IoError(e.to_string()))?;
Ok(content
.lines()
.filter(|l| l.contains("filter=lfs"))
.map(|l| l.trim().to_string())
.collect())
}
pub fn lfs_gitattributes_add(&self, pattern: &str) -> GitResult<()> {
let line = format!("{} filter=lfs diff=lfs merge=lfs -text", pattern);
let path = self.gitattributes_path();
let content = if path.exists() {
fs::read_to_string(&path).map_err(|e| GitError::IoError(e.to_string()))?
} else {
String::new()
};
if content.lines().any(|l| l.trim() == line) {
return Ok(());
}
let new_content = if content.ends_with('\n') || content.is_empty() {
format!("{}{}\n", content, line)
} else {
format!("{}\n{}\n", content, line)
};
fs::write(&path, new_content).map_err(|e| GitError::IoError(e.to_string()))?;
Ok(())
}
pub fn lfs_gitattributes_remove(&self, pattern: &str) -> GitResult<bool> {
let path = self.gitattributes_path();
if !path.exists() {
return Ok(false);
}
let content = fs::read_to_string(&path).map_err(|e| GitError::IoError(e.to_string()))?;
let target = format!("{} filter=lfs diff=lfs merge=lfs -text", pattern);
let new_lines: Vec<&str> = content.lines().filter(|l| l.trim() != target).collect();
if new_lines.len() == content.lines().count() {
return Ok(false);
}
let new_content = new_lines.join("\n");
fs::write(&path, new_content).map_err(|e| GitError::IoError(e.to_string()))?;
Ok(true)
}
pub fn lfs_gitattributes_match(&self, path_str: &str) -> GitResult<bool> {
let patterns = self.lfs_gitattributes_list()?;
for pattern in patterns {
let glob_str = pattern.split_whitespace().next().unwrap_or(&pattern);
if let Ok(glob) = Glob::new(glob_str) {
let matcher = glob.compile_matcher();
if matcher.is_match(path_str) {
return Ok(true);
}
}
}
Ok(false)
}
fn lfs_objects_dir(&self) -> PathBuf {
self.repo().path().join("lfs").join("objects")
}
/// Validates that the OID is at least 4 characters for path splitting.
fn lfs_validate_oid(&self, oid: &LfsOid) -> GitResult<()> {
if oid.as_str().len() < 4 {
return Err(GitError::Internal(format!(
"LFS OID too short for path splitting: {}",
oid
)));
}
Ok(())
}
pub fn lfs_object_cached(&self, oid: &LfsOid) -> bool {
if oid.as_str().len() < 4 {
return false;
}
let (p1, rest) = oid.as_str().split_at(2);
let (p2, _) = rest.split_at(2);
self.lfs_objects_dir()
.join(p1)
.join(p2)
.join(oid.as_str())
.exists()
}
pub fn lfs_object_path(&self, oid: &LfsOid) -> GitResult<PathBuf> {
self.lfs_validate_oid(oid)?;
let (p1, rest) = oid.as_str().split_at(2);
let (p2, _) = rest.split_at(2);
Ok(self.lfs_objects_dir().join(p1).join(p2).join(oid.as_str()))
}
pub fn lfs_object_put(&self, oid: &LfsOid, content: &[u8]) -> GitResult<PathBuf> {
use std::io::Write;
let path = self.lfs_object_path(oid)?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| GitError::IoError(e.to_string()))?;
}
let mut file = fs::File::create(&path).map_err(|e| GitError::IoError(e.to_string()))?;
file.write_all(content)
.map_err(|e| GitError::IoError(e.to_string()))?;
Ok(path)
}
pub fn lfs_object_get(&self, oid: &LfsOid) -> GitResult<Vec<u8>> {
let path = self.lfs_object_path(oid)?;
if !path.exists() {
return Err(GitError::LfsError(format!(
"object {} not found in local cache",
oid
)));
}
fs::read(&path).map_err(|e| GitError::IoError(e.to_string()))
}
pub fn lfs_object_list(&self) -> GitResult<Vec<LfsOid>> {
let base = self.lfs_objects_dir();
if !base.exists() {
return Ok(Vec::new());
}
let mut oids = Vec::new();
self.lfs_object_list_impl(&base, &mut oids)?;
Ok(oids)
}
fn lfs_object_list_impl(&self, dir: &Path, oids: &mut Vec<LfsOid>) -> GitResult<()> {
for entry in fs::read_dir(dir).map_err(|e| GitError::IoError(e.to_string()))? {
let entry = entry.map_err(|e| GitError::IoError(e.to_string()))?;
let path = entry.path();
if path.is_dir() {
self.lfs_object_list_impl(&path, oids)?;
} else if path.is_file() {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
oids.push(LfsOid::new(name));
}
}
}
Ok(())
}
pub fn lfs_object_delete(&self, oid: &LfsOid) -> GitResult<bool> {
let path = self.lfs_object_path(oid)?;
if path.exists() {
fs::remove_file(&path).map_err(|e| GitError::IoError(e.to_string()))?;
Ok(true)
} else {
Ok(false)
}
}
pub fn lfs_cache_size(&self) -> GitResult<u64> {
let oids = self.lfs_object_list()?;
let mut total = 0u64;
for oid in oids {
if let Ok(path) = self.lfs_object_path(&oid) {
if let Ok(meta) = fs::metadata(&path) {
total += meta.len();
}
}
}
Ok(total)
}
pub fn lfs_config(&self) -> GitResult<LfsConfig> {
let root = self.repo().workdir().unwrap_or_else(|| self.repo().path());
let path = root.join(".lfsconfig");
if !path.exists() {
return Ok(LfsConfig::new());
}
let content = fs::read_to_string(&path).map_err(|e| GitError::IoError(e.to_string()))?;
let mut config = LfsConfig::new();
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((k, v)) = line.split_once('=') {
let k = k.trim();
let v = v.trim();
if k == "lfs.url" || k == "lfs.endpoint" {
config.endpoint = Some(v.to_string());
} else if k == "lfs.accesskey" || k == "lfs.access_token" {
config.access_token = Some(v.to_string());
}
}
}
Ok(config)
}
pub fn lfs_config_set(&self, config: &LfsConfig) -> GitResult<()> {
let root = self.repo().workdir().unwrap_or_else(|| self.repo().path());
let path = root.join(".lfsconfig");
let mut lines = Vec::new();
if let Some(ref ep) = config.endpoint {
lines.push(format!("lfs.url = {}", ep));
}
if let Some(ref tok) = config.access_token {
lines.push(format!("lfs.access_token = {}", tok));
}
fs::write(&path, lines.join("\n") + "\n").map_err(|e| GitError::IoError(e.to_string()))?;
Ok(())
}
}