chore: migrate from git command to gix (#16)
Rust / build (macos-latest, stable) (push) Canceled after 0s
Rust / build (ubuntu-latest, stable) (push) Canceled after 0s
Rust / build (windows-latest, stable) (push) Canceled after 0s

Reviewed-on: https://git.reya.su/reya/signed/pulls/16
This commit was merged in pull request #16.
This commit is contained in:
2026-09-08 04:01:13 +00:00
parent 1af4c66566
commit a8e19e5fcd
6 changed files with 866 additions and 281 deletions
+2
View File
@@ -9,6 +9,8 @@ signed_core = { path = "../signed_core" }
nostr.workspace = true
gix = { workspace = true, features = ["revision", "blob-diff"] }
gix-worktree = "0.56"
gix-worktree-state = "0.34"
anyhow.workspace = true
[dev-dependencies]
File diff suppressed because it is too large Load Diff
+14 -66
View File
@@ -1,6 +1,5 @@
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use anyhow::Error;
@@ -617,67 +616,27 @@ fn resolve_associations<'a>(
out
}
/// Whether the worktree of `path` has uncommitted changes.
fn worktree_dirty(path: &Path) -> bool {
let output = Command::new("git")
.arg("-C")
.arg(path)
.args(["status", "--porcelain"])
.env("GIT_TERMINAL_PROMPT", "0")
.output();
match output {
Ok(output) => !String::from_utf8_lossy(&output.stdout).trim().is_empty(),
Err(_) => false,
}
}
/// Commits in `base..branch` of the checkout at `path`.
fn commits_ahead(path: &Path, base: &str, branch: &str) -> u32 {
let output = Command::new("git")
.arg("-C")
.arg(path)
.args(["rev-list", "--count", &format!("{base}..{branch}")])
.env("GIT_TERMINAL_PROMPT", "0")
.output();
match output {
Ok(output) => String::from_utf8_lossy(&output.stdout)
.trim()
.parse()
.unwrap_or(0),
Err(_) => 0,
}
}
/// The branch checked out at `path`, read via `git branch --show-current`.
fn current_branch_of(path: &Path) -> Option<String> {
let output = Command::new("git")
.arg("-C")
.arg(path)
.args(["branch", "--show-current"])
.env("GIT_TERMINAL_PROMPT", "0")
.output()
.ok()?;
let branch = String::from_utf8_lossy(&output.stdout).trim().to_owned();
(!branch.is_empty()).then_some(branch)
}
/// The ready-to-contribute status of one checkout.
fn checkout_status(path: &Path, announced_head: Option<&str>) -> Option<CheckoutStatus> {
let branches = signed_git::worktree_branches(path).ok()?;
if branches.is_empty() || worktree_dirty(path) {
if branches.is_empty() || signed_git::worktree_dirty(path) {
return None;
}
let branch = current_branch_of(path)?;
let branch = signed_git::worktree_current_branch(path)?;
let head = signed_git::head_commit_id(path).ok().flatten()?;
let base = announced_head
.filter(|name| branches.iter().any(|b| b == name))
.map(str::to_owned)
.or_else(|| branches.iter().find(|b| *b == "main").cloned())
.or_else(|| branches.first().cloned())?;
if base == branch {
return None;
}
let ahead = commits_ahead(path, &base, &branch);
let ahead = signed_git::worktree_commits_ahead(path, &base, &branch);
(ahead > 0).then_some(CheckoutStatus {
path: path.to_path_buf(),
branch,
@@ -687,30 +646,17 @@ fn checkout_status(path: &Path, announced_head: Option<&str>) -> Option<Checkout
})
}
/// Whether the reference `name` exists in the checkout at `path`.
///
/// Example, `refs/remotes/origin/main`.
fn ref_exists(path: &Path, name: &str) -> bool {
let output = Command::new("git")
.arg("-C")
.arg(path)
.args(["rev-parse", "--verify", "--quiet", name])
.env("GIT_TERMINAL_PROMPT", "0")
.output();
matches!(output, Ok(output) if output.status.success())
}
/// The `ready to push` status of one checkout of the user's own repository.
///
/// `fetch` refreshes the remote heads first, so a full pass sees pushes made
/// elsewhere; the fast local pass skips it and compares against the tracking
/// refs left by the last full pass, which is enough to detect local commits.
fn checkout_push_status(path: &Path, fetch: bool) -> Option<CheckoutStatus> {
if worktree_dirty(path) {
if signed_git::worktree_dirty(path) {
return None;
}
let branch = current_branch_of(path)?;
let branch = signed_git::worktree_current_branch(path)?;
let head = signed_git::head_commit_id(path).ok().flatten()?;
let origin = signed_git::origin_url(path).ok().flatten()?;
@@ -724,15 +670,15 @@ fn checkout_push_status(path: &Path, fetch: bool) -> Option<CheckoutStatus> {
// A branch never fetched or pushed yet compares against the remote HEAD.
// The remote HEAD is the fork point in practice.
let base = if ref_exists(path, &remote) {
let base = if signed_git::worktree_ref_exists(path, &remote) {
remote
} else if ref_exists(path, "refs/remotes/origin/HEAD") {
} else if signed_git::worktree_ref_exists(path, "refs/remotes/origin/HEAD") {
"refs/remotes/origin/HEAD".to_owned()
} else {
return None;
};
let ahead = commits_ahead(path, &base, &branch);
let ahead = signed_git::worktree_commits_ahead(path, &base, &branch);
(ahead > 0).then_some(CheckoutStatus {
path: path.to_path_buf(),
@@ -822,6 +768,8 @@ pub fn pr_proposes_checkout(
#[cfg(test)]
mod tests {
use std::process::Command;
use signed_core::{RepoAddr, repo_addr};
use super::*;
+118 -9
View File
@@ -492,16 +492,8 @@ impl RepoDetailView {
if refresh_generation != this.ref_generation {
return;
}
if let Ok(Some((moved, branches, tags, current_branch, head_commit))) = refresh {
if moved {
// The mirror caught up with the remote.
// E.g. the push of an owned checkout just landed.
// Rebuild the explorer, previews and commit list from the worktree.
this.reload_worktree(cx);
cx.notify();
return;
}
if let Ok(Some((moved, branches, tags, current_branch, head_commit))) = refresh {
let branches: Vec<SharedString> = branches.iter().map(Into::into).collect();
let tags: Vec<SharedString> = tags.iter().map(Into::into).collect();
@@ -528,6 +520,10 @@ impl RepoDetailView {
this.load_all_commits(cx);
}
if moved {
this.catch_up_worktree(cx);
}
cx.notify();
}
})?;
@@ -1225,6 +1221,119 @@ impl RepoDetailView {
self.tasks.push(task);
}
/// Refresh the file explorer, previews and commit list after the mirror
/// caught up with the remote.
///
/// The checked-out branch fast-forwarded in place, so unlike
/// [`Self::reload_worktree`] this keeps the panel's selection and previews:
/// it rebuilds the tree, drops previews of files the refresh removed and
/// re-renders the README when it is on screen.
fn catch_up_worktree(&mut self, cx: &mut Context<Self>) {
let Some(worktree) = self.worktree.clone() else {
return;
};
let task = cx.spawn(async move |this, cx| {
let result = cx
.background_spawn(async move {
let snapshot = signed_git::worktree_snapshot(&worktree)?;
let tree = build_tree_items(&snapshot.entries);
Ok::<_, Error>((snapshot, tree))
})
.await;
this.update(cx, |this, cx| {
match result {
Ok((snapshot, tree)) => {
let head_changed = snapshot.head_commit.as_ref().map(|c| &c.id)
!= this.head_commit.as_ref().map(|c| &c.id);
this.head_commit = snapshot.head_commit;
this.tree_state.update(cx, |state, cx| {
state.set_items(tree_items(tree, false), cx);
});
// Drop previews of files the refresh removed from the worktree,
// everything else stays put.
let present: HashSet<String> = snapshot
.entries
.iter()
.map(|path| path.to_string_lossy().into_owned())
.collect();
let mut previewed: Vec<String> = Vec::new();
previewed.extend(this.files.keys().cloned());
previewed.extend(this.selected_file.clone().map(|p| p.to_string()));
if let Some(path) = this.md.as_ref().and_then(|md| md.path.clone()) {
previewed.push(path.to_string());
}
if let Some(path) = this.code.as_ref().map(|code| code.path.clone()) {
previewed.push(path.to_string());
}
previewed.sort();
previewed.dedup();
for path in previewed {
if !present.contains(&path) {
this.drop_preview_of(&path);
}
}
// Re-render the README when it is on screen, i.e. when no file preview is open.
if this.selected_file.is_none() {
match snapshot.readme_path.zip(snapshot.readme) {
Some((path, bytes)) => {
this.readme_name = Some(path.to_string_lossy().into());
if let Ok(text) = String::from_utf8(bytes) {
this.set_markdown(None, &text, cx);
}
}
None => {
this.md = None;
this.readme_name = None;
}
}
}
if head_changed {
this.all_commits = None;
this.loading_all_commits = false;
this.load_all_commits(cx);
}
}
Err(error) => {
this.error = Some(error.to_string().into());
}
}
cx.notify();
})?;
Ok(())
});
self.tasks.push(task);
}
/// Drop the cached preview, editor and commit state of `path`.
fn drop_preview_of(&mut self, path: &str) {
if let Some(FileContent::Text(text)) = self.files.remove(path) {
self.preview_bytes -= text.len();
}
self.commits.remove(path);
if self.selected_file.as_deref() == Some(path) {
self.selected_file = None;
}
if self.md.as_ref().and_then(|md| md.path.as_deref()) == Some(path) {
self.md = None;
}
if self.code.as_ref().map(|code| code.path.as_ref()) == Some(path) {
self.code = None;
}
}
/// Drop the oldest previews beyond the cache caps.
/// Keep the currently selected file.
/// An evicted file's parsed editor state drops with its entry.