Show remote activity on iPhone Home

This commit is contained in:
hugo committed 2026-08-11 17:12:33 +02:00
1 parent daf122aff3
commit 8e883f5ccf
9 files changed
+3377 -35

No files matched your search

+188 -25
View File
@@ -180,6 +180,53 @@ pub struct GitSnapshot {
recent: Vec<GitLogEntry>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GitCommitActivity {
commit: GitLogEntry,
changes: Vec<GitChange>,
}
impl GitCommitActivity {
pub fn commit(&self) -> &GitLogEntry {
&self.commit
}
pub fn changes(&self) -> &[GitChange] {
&self.changes
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GitDivergence {
branch: String,
status: GitStatus,
remote: GitRemoteStatus,
incoming: Vec<GitCommitActivity>,
outgoing: Vec<GitCommitActivity>,
}
impl GitDivergence {
pub fn branch(&self) -> &str {
&self.branch
}
pub fn status(&self) -> &GitStatus {
&self.status
}
pub fn remote(&self) -> &GitRemoteStatus {
&self.remote
}
pub fn incoming(&self) -> &[GitCommitActivity] {
&self.incoming
}
pub fn outgoing(&self) -> &[GitCommitActivity] {
&self.outgoing
}
}
impl GitSnapshot {
pub fn root(&self) -> &Path {
&self.root
@@ -1698,32 +1745,144 @@ impl GitRepository {
})
}
/// Return commits and typed path changes on each side of the configured
/// remote-tracking branch without performing network access.
pub fn divergence(
&self,
configured: &GitRemote,
activity_limit: usize,
) -> Result<GitDivergence, GitError> {
let branch = self.current_branch()?;
let status = self.status()?;
let name = configured.name().as_str();
let actual_url = self.remote_url(name)?;
if actual_url != configured.url().as_str() {
return Err(GitError::ForbiddenRemoteUrl);
}
let local_id = self
.repository
.head_id()
.map_err(|_| GitError::UnbornHead)?
.detach();
let remote_ref = format!("refs/remotes/{name}/{branch}");
let remote_id = self
.repository
.find_reference(&remote_ref)
.map_err(|_| GitError::RemoteNotFound {
name: remote_ref.clone(),
})?
.into_fully_peeled_id()
.map_err(invalid)?
.detach();
let local_ids = self.ancestor_ids(local_id)?;
let remote_ids = self.ancestor_ids(remote_id)?;
let ahead = local_ids.difference(&remote_ids).count();
let behind = remote_ids.difference(&local_ids).count();
let incoming = self.commit_activities(remote_id, &local_ids, activity_limit)?;
let outgoing = self.commit_activities(local_id, &remote_ids, activity_limit)?;
Ok(GitDivergence {
branch,
status,
remote: GitRemoteStatus {
name: name.to_owned(),
url: actual_url,
ahead,
behind,
},
incoming,
outgoing,
})
}
fn commit_activities(
&self,
tip: gix::hash::ObjectId,
excluded: &BTreeSet<gix::hash::ObjectId>,
limit: usize,
) -> Result<Vec<GitCommitActivity>, GitError> {
if limit == 0 {
return Ok(Vec::new());
}
let commit = self
.repository
.find_object(tip)
.map_err(invalid)?
.peel_to_commit()
.map_err(invalid)?;
let mut output = Vec::new();
for info in commit.ancestors().all().map_err(invalid)? {
let info = info.map_err(invalid)?;
if excluded.contains(&info.id) {
continue;
}
output.push(self.commit_activity(info.id)?);
if output.len() >= limit {
break;
}
}
Ok(output)
}
fn commit_activity(&self, id: gix::hash::ObjectId) -> Result<GitCommitActivity, GitError> {
let commit = self
.repository
.find_object(id)
.map_err(invalid)?
.peel_to_commit()
.map_err(invalid)?;
let new_tree = commit.tree_id().map_err(invalid)?.detach();
let old_tree = commit
.parent_ids()
.next()
.map(|parent| {
self.repository
.find_object(parent.detach())
.map_err(invalid)?
.peel_to_commit()
.map_err(invalid)?
.tree_id()
.map_err(invalid)
.map(|tree| tree.detach())
})
.transpose()?;
let old = tree_map_by_id(&self.repository, old_tree)?;
let new = tree_map_by_id(&self.repository, Some(new_tree))?;
Ok(GitCommitActivity {
commit: git_log_entry(&commit)?,
changes: compare_maps(&old, &new),
})
}
fn ahead_behind(
&self,
local: gix::hash::ObjectId,
remote: gix::hash::ObjectId,
) -> Result<(usize, usize), GitError> {
let ancestors = |id| -> Result<BTreeSet<gix::hash::ObjectId>, GitError> {
let commit = self
.repository
.find_object(id)
.map_err(invalid)?
.peel_to_commit()
.map_err(invalid)?;
let mut ids = BTreeSet::from([id]);
for info in commit.ancestors().all().map_err(invalid)? {
ids.insert(info.map_err(invalid)?.id);
}
Ok(ids)
};
let local_ids = ancestors(local)?;
let remote_ids = ancestors(remote)?;
let local_ids = self.ancestor_ids(local)?;
let remote_ids = self.ancestor_ids(remote)?;
Ok((
local_ids.difference(&remote_ids).count(),
remote_ids.difference(&local_ids).count(),
))
}
fn ancestor_ids(
&self,
id: gix::hash::ObjectId,
) -> Result<BTreeSet<gix::hash::ObjectId>, GitError> {
let commit = self
.repository
.find_object(id)
.map_err(invalid)?
.peel_to_commit()
.map_err(invalid)?;
let mut ids = BTreeSet::from([id]);
for info in commit.ancestors().all().map_err(invalid)? {
ids.insert(info.map_err(invalid)?.id);
}
Ok(ids)
}
fn update_remote_tracking(
&self,
remote: &str,
@@ -2329,16 +2488,7 @@ impl GitRepository {
}
let info = info.map_err(invalid)?;
let commit = info.object().map_err(invalid)?;
let decoded = commit.decode().map_err(invalid)?;
let author = decoded.author().map_err(invalid)?;
output.push(GitLogEntry {
id: commit.id.to_string(),
parents: decoded.parents().map(|id| id.to_string()).collect(),
author_name: author.name.to_str_lossy().into_owned(),
author_email: author.email.to_str_lossy().into_owned(),
message: decoded.message.to_str_lossy().into_owned(),
timestamp: author.time().map_err(invalid)?.seconds,
});
output.push(git_log_entry(&commit)?);
}
Ok(output)
}
@@ -2516,6 +2666,19 @@ fn append_diff_lines(output: &mut Vec<u8>, prefix: u8, contents: &[u8]) {
}
}
fn git_log_entry(commit: &gix::Commit<'_>) -> Result<GitLogEntry, GitError> {
let decoded = commit.decode().map_err(invalid)?;
let author = decoded.author().map_err(invalid)?;
Ok(GitLogEntry {
id: commit.id.to_string(),
parents: decoded.parents().map(|id| id.to_string()).collect(),
author_name: author.name.to_str_lossy().into_owned(),
author_email: author.email.to_str_lossy().into_owned(),
message: decoded.message.to_str_lossy().into_owned(),
timestamp: author.time().map_err(invalid)?.seconds,
})
}
impl EntryCommitter for GitRepository {
fn commit(&mut self, change: &EntryCommit) -> Result<(), EntryCommitError> {
self.stage_and_commit(&[change.path().encrypted_relative_path()], change.message())