add sync status classification for branches
This commit is contained in:
@@ -5,6 +5,7 @@ mod nip34;
|
||||
mod patch;
|
||||
mod repo;
|
||||
mod scan;
|
||||
mod sync;
|
||||
mod worktree;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -17,6 +18,7 @@ pub use nip34::{GraspSignals, Nip34Binding, Nip34Kind};
|
||||
pub use patch::PatchParser;
|
||||
pub use repo::{Repo, RepoRefState};
|
||||
pub use scan::{LocalRepo, find_git_repos};
|
||||
pub use sync::{RefSync, RepoSyncStatus};
|
||||
pub use worktree::WorktreeSnapshot;
|
||||
|
||||
pub(crate) trait GixResultExt<T> {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::{BTreeSet, HashMap, HashSet};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Stdio;
|
||||
@@ -13,6 +13,7 @@ use crate::GixResultExt as _;
|
||||
use crate::diff::{CommitDiff, DiffStatus, FileDiff, HunkCollector};
|
||||
use crate::history::{CommitList, FileCommit, MAX_LISTED_COMMITS};
|
||||
use crate::nip34::{GraspSignals, Nip34Binding, Nip34Json, Nip34Kind};
|
||||
use crate::sync::{RefSync, RepoSyncStatus};
|
||||
use crate::worktree::WorktreeSnapshot;
|
||||
|
||||
const OBJECT_CACHE_BYTES: usize = 64 * 1024 * 1024;
|
||||
@@ -797,6 +798,86 @@ impl Repo {
|
||||
self.inner.rev_parse_single(rev.as_bytes()).ok()
|
||||
}
|
||||
|
||||
/// Classifies every local branch against the Nostr state refs, ignoring tags.
|
||||
pub fn sync_status(&self, remote_refs: &[(String, String)]) -> Result<RepoSyncStatus> {
|
||||
let local_refs = self.ref_state()?;
|
||||
let local = RepoSyncStatus::branches(&local_refs.refs);
|
||||
let remote = RepoSyncStatus::branches(remote_refs);
|
||||
let names: BTreeSet<&str> = local.keys().chain(remote.keys()).copied().collect();
|
||||
|
||||
let mut refs = Vec::new();
|
||||
let mut ahead_total = 0;
|
||||
let mut behind_total = 0;
|
||||
|
||||
for name in names {
|
||||
let sync = match (local.get(name), remote.get(name)) {
|
||||
(Some(local_commit), Some(remote_commit)) => {
|
||||
self.branch_sync(local_commit, remote_commit)
|
||||
}
|
||||
(Some(_), None) => RefSync::LocalOnly,
|
||||
(None, Some(_)) => RefSync::RemoteOnly,
|
||||
(None, None) => continue,
|
||||
};
|
||||
|
||||
match &sync {
|
||||
RefSync::LocalAhead { ahead } => ahead_total += ahead,
|
||||
RefSync::RemoteAhead { behind } => behind_total += behind,
|
||||
RefSync::Diverged { ahead, behind } => {
|
||||
ahead_total += ahead;
|
||||
behind_total += behind;
|
||||
}
|
||||
RefSync::InSync | RefSync::LocalOnly | RefSync::RemoteOnly => {}
|
||||
}
|
||||
|
||||
refs.push((name.to_owned(), sync));
|
||||
}
|
||||
|
||||
Ok(RepoSyncStatus {
|
||||
refs,
|
||||
ahead_total,
|
||||
behind_total,
|
||||
})
|
||||
}
|
||||
|
||||
/// Classifies a branch present on both sides.
|
||||
fn branch_sync(&self, local_commit: &str, remote_commit: &str) -> RefSync {
|
||||
if local_commit == remote_commit {
|
||||
return RefSync::InSync;
|
||||
}
|
||||
|
||||
match self.merge_base(local_commit, remote_commit) {
|
||||
Ok(Some(base)) if base == remote_commit => RefSync::LocalAhead {
|
||||
ahead: self.commits_ahead(&base, local_commit) as usize,
|
||||
},
|
||||
Ok(Some(base)) if base == local_commit => RefSync::RemoteAhead {
|
||||
behind: self.commits_ahead(&base, remote_commit) as usize,
|
||||
},
|
||||
Ok(Some(base)) => RefSync::Diverged {
|
||||
ahead: self.commits_ahead(&base, local_commit) as usize,
|
||||
behind: self.commits_ahead(&base, remote_commit) as usize,
|
||||
},
|
||||
// Unrelated histories share no base, so every commit counts.
|
||||
Ok(None) => RefSync::Diverged {
|
||||
ahead: self.count_reachable(local_commit),
|
||||
behind: self.count_reachable(remote_commit),
|
||||
},
|
||||
// Nostr tip missing locally, so at least that commit differs.
|
||||
Err(_) => RefSync::RemoteAhead { behind: 1 },
|
||||
}
|
||||
}
|
||||
|
||||
/// Counts commits reachable from a commit id.
|
||||
fn count_reachable(&self, commit: &str) -> usize {
|
||||
let Ok(commit_id) = self.inner.rev_parse_single(commit.as_bytes()) else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
match self.inner.rev_walk([commit_id]).all() {
|
||||
Ok(walk) => walk.filter_map(Result::ok).count(),
|
||||
Err(_) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Lists every worktree path relative to the root, directories first.
|
||||
pub fn entries(&self) -> Result<Vec<PathBuf>> {
|
||||
let workdir = self.inner.workdir().context("repository has no worktree")?;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// How a single branch compares between local and Nostr.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum RefSync {
|
||||
InSync,
|
||||
LocalAhead { ahead: usize },
|
||||
RemoteAhead { behind: usize },
|
||||
Diverged { ahead: usize, behind: usize },
|
||||
LocalOnly,
|
||||
RemoteOnly,
|
||||
}
|
||||
|
||||
/// Branch comparison between a local repository and its Nostr state.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RepoSyncStatus {
|
||||
/// Branch short name and its classification.
|
||||
pub refs: Vec<(String, RefSync)>,
|
||||
pub ahead_total: usize,
|
||||
pub behind_total: usize,
|
||||
}
|
||||
|
||||
impl RepoSyncStatus {
|
||||
/// True when every branch matches the Nostr state.
|
||||
pub fn in_sync(&self) -> bool {
|
||||
self.refs.iter().all(|(_, sync)| *sync == RefSync::InSync)
|
||||
}
|
||||
|
||||
/// Maps branch short name to lowercase commit oid.
|
||||
pub(crate) fn branches(refs: &[(String, String)]) -> BTreeMap<&str, String> {
|
||||
refs.iter()
|
||||
.filter_map(|(name, commit)| {
|
||||
name.strip_prefix("refs/heads/")
|
||||
.map(|branch| (branch, commit.to_ascii_lowercase()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user