Files
ElyOffice/apps/ely-tree/src/structure.rs
T
ZacharyZhang-NY b7fca0146d Ely Office: six offline-first desktop apps in Rust and GPUI
ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git),
ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
2026-10-08 21:20:08 -04:00

162 lines
5.2 KiB
Rust

//! Worktrees, submodules and LFS files (TREE-12).
use std::{
io,
path::{Path, PathBuf},
};
use crate::git::Git;
fn bad(what: &str) -> io::Error {
io::Error::new(
io::ErrorKind::InvalidData,
format!("an unread listing {what:?}"),
)
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Worktree {
pub path: PathBuf,
pub head: Option<String>,
/// Short name; none when detached.
pub branch: Option<String>,
pub bare: bool,
/// Why it is locked, maybe empty.
pub locked: Option<String>,
/// Why Git would prune it.
pub prunable: Option<String>,
}
/// `worktree list --porcelain -z`: fields, records end empty.
pub fn worktrees(git: &Git, root: &Path) -> io::Result<Vec<Worktree>> {
let out = git.run(root, &["worktree", "list", "--porcelain", "-z"])?;
let text = std::str::from_utf8(&out).map_err(|_| bad("not UTF-8"))?;
let mut found = Vec::new();
let mut now: Option<Worktree> = None;
for field in text.split('\0') {
if field.is_empty() {
found.extend(now.take());
continue;
}
let (key, value) = field.split_once(' ').unwrap_or((field, ""));
let tree = match (&mut now, key) {
(None, "worktree") => {
now = Some(Worktree {
path: PathBuf::from(value),
..Worktree::default()
});
continue;
}
(Some(tree), _) => tree,
(None, _) => return Err(bad(field)),
};
match key {
"HEAD" => tree.head = Some(value.to_string()),
"branch" => {
let short = value.strip_prefix("refs/heads/").unwrap_or(value);
tree.branch = Some(short.to_string());
}
"bare" => tree.bare = true,
"locked" => tree.locked = Some(value.to_string()),
"prunable" => tree.prunable = Some(value.to_string()),
"detached" => {}
_ => return Err(bad(field)),
}
}
found.extend(now);
Ok(found)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Checkout {
/// Not cloned into its folder yet.
Missing,
At(String),
Unread(String),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Submodule {
pub path: String,
/// The commit this repository records.
pub recorded: String,
pub conflicted: bool,
pub checkout: Checkout,
}
/// Gitlinks in the index, and what each folder holds.
pub fn submodules(git: &Git, root: &Path) -> io::Result<Vec<Submodule>> {
let out = git.run(root, &["ls-files", "--stage", "-z"])?;
let text = std::str::from_utf8(&out).map_err(|_| bad("not UTF-8"))?;
let mut found: Vec<Submodule> = Vec::new();
for entry in text.split('\0').filter(|entry| !entry.is_empty()) {
let (head, path) = entry.split_once('\t').ok_or_else(|| bad(entry))?;
let mut parts = head.split(' ');
let (Some(mode), Some(oid), Some(stage)) = (parts.next(), parts.next(), parts.next())
else {
return Err(bad(entry));
};
if mode != "160000" {
continue;
}
if let Some(known) = found.iter_mut().find(|known| known.path == path) {
known.conflicted = true;
continue;
}
let folder = root.join(path);
// Without its own `.git`, Git here would be ours.
let checkout = if !folder.join(".git").exists() {
Checkout::Missing
} else {
match git.run(&folder, &["rev-parse", "HEAD"]) {
Ok(head) => Checkout::At(String::from_utf8_lossy(&head).trim().to_string()),
Err(error) => Checkout::Unread(error.to_string()),
}
};
found.push(Submodule {
path: path.to_string(),
recorded: oid.to_string(),
conflicted: stage != "0",
checkout,
});
}
Ok(found)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LfsFile {
pub path: String,
pub size: u64,
/// The work tree holds the content, not the pointer.
pub checked_out: bool,
/// The object is in this repository's store.
pub downloaded: bool,
}
/// `git lfs ls-files` as JSON; `files` is null with none.
pub fn lfs(git: &Git, root: &Path) -> io::Result<Vec<LfsFile>> {
let out = git.run(root, &["lfs", "ls-files", "--long", "--json"])?;
let read: serde_json::Value = serde_json::from_slice(&out).map_err(io::Error::other)?;
let files = match &read["files"] {
serde_json::Value::Null => return Ok(Vec::new()),
serde_json::Value::Array(files) => files,
other => return Err(bad(&other.to_string())),
};
files
.iter()
.map(|file| {
Ok(LfsFile {
path: file["name"]
.as_str()
.ok_or_else(|| bad("name"))?
.to_string(),
size: file["size"].as_u64().ok_or_else(|| bad("size"))?,
checked_out: file["checkout"].as_bool().ok_or_else(|| bad("checkout"))?,
downloaded: file["downloaded"]
.as_bool()
.ok_or_else(|| bad("downloaded"))?,
})
})
.collect()
}