321 lines
11 KiB
Rust
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(())
|
|
}
|
|
}
|