feat: add sync detection for repositories #28
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
* Prioritize code correctness and clarity. Speed and efficiency are secondary priorities unless otherwise specified.
|
* Prioritize code correctness and clarity. Speed and efficiency are secondary priorities unless otherwise specified.
|
||||||
* Do not write organizational or comments that summarize the code. Comments should only be written in order to explain "why" the code is written in some way in the case there is a reason that is tricky / non-obvious.
|
* Do not write organizational or comments that summarize the code. Comments should only be written in order to explain "why" the code is written in some way in the case there is a reason that is tricky / non-obvious.
|
||||||
|
* Keep doc comments and comments simple, one line, straightforward, and unbloated.
|
||||||
* Prefer implementing functionality in existing files unless it is a new logical component. Avoid creating many small files.
|
* Prefer implementing functionality in existing files unless it is a new logical component. Avoid creating many small files.
|
||||||
|
* Prefer `impl` blocks over standalone functions, place functionality as methods or associated functions on the relevant type, extending the type's existing `impl` block instead of creating a duplicate one in another file.
|
||||||
* Avoid using functions that panic like `unwrap()`, instead use mechanisms like `?` to propagate errors.
|
* Avoid using functions that panic like `unwrap()`, instead use mechanisms like `?` to propagate errors.
|
||||||
* Be careful with operations like indexing which may panic if the indexes are out of bounds.
|
* Be careful with operations like indexing which may panic if the indexes are out of bounds.
|
||||||
* Never silently discard errors with `let _ =` on fallible operations. Always handle errors appropriately:
|
* Never silently discard errors with `let _ =` on fallible operations. Always handle errors appropriately:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ mod nip34;
|
|||||||
mod patch;
|
mod patch;
|
||||||
mod repo;
|
mod repo;
|
||||||
mod scan;
|
mod scan;
|
||||||
|
mod sync;
|
||||||
mod worktree;
|
mod worktree;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -17,6 +18,7 @@ pub use nip34::{GraspSignals, Nip34Binding, Nip34Kind};
|
|||||||
pub use patch::PatchParser;
|
pub use patch::PatchParser;
|
||||||
pub use repo::{Repo, RepoRefState};
|
pub use repo::{Repo, RepoRefState};
|
||||||
pub use scan::{LocalRepo, find_git_repos};
|
pub use scan::{LocalRepo, find_git_repos};
|
||||||
|
pub use sync::{RefSync, RepoSyncStatus};
|
||||||
pub use worktree::WorktreeSnapshot;
|
pub use worktree::WorktreeSnapshot;
|
||||||
|
|
||||||
pub(crate) trait GixResultExt<T> {
|
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::io::Write;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
@@ -13,6 +13,7 @@ use crate::GixResultExt as _;
|
|||||||
use crate::diff::{CommitDiff, DiffStatus, FileDiff, HunkCollector};
|
use crate::diff::{CommitDiff, DiffStatus, FileDiff, HunkCollector};
|
||||||
use crate::history::{CommitList, FileCommit, MAX_LISTED_COMMITS};
|
use crate::history::{CommitList, FileCommit, MAX_LISTED_COMMITS};
|
||||||
use crate::nip34::{GraspSignals, Nip34Binding, Nip34Json, Nip34Kind};
|
use crate::nip34::{GraspSignals, Nip34Binding, Nip34Json, Nip34Kind};
|
||||||
|
use crate::sync::{RefSync, RepoSyncStatus};
|
||||||
use crate::worktree::WorktreeSnapshot;
|
use crate::worktree::WorktreeSnapshot;
|
||||||
|
|
||||||
const OBJECT_CACHE_BYTES: usize = 64 * 1024 * 1024;
|
const OBJECT_CACHE_BYTES: usize = 64 * 1024 * 1024;
|
||||||
@@ -797,6 +798,86 @@ impl Repo {
|
|||||||
self.inner.rev_parse_single(rev.as_bytes()).ok()
|
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.
|
/// Lists every worktree path relative to the root, directories first.
|
||||||
pub fn entries(&self) -> Result<Vec<PathBuf>> {
|
pub fn entries(&self) -> Result<Vec<PathBuf>> {
|
||||||
let workdir = self.inner.workdir().context("repository has no worktree")?;
|
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