feat: multi-steps push progress #30

Merged
reya merged 7 commits from adv-sync into master 2026-10-14 02:08:22 +00:00
8 changed files with 99 additions and 36 deletions
Showing only changes of commit 5b16ba50c2 - Show all commits
-2
View File
@@ -5,7 +5,6 @@ pub mod filters;
pub mod git_event;
pub mod inbox;
pub mod pull_request;
pub mod state;
pub mod status;
pub mod upstream;
@@ -16,6 +15,5 @@ pub use filters::Filters;
pub use git_event::GitEvent;
pub use inbox::{InboxItem, InboxReadState, ThreadResolver};
pub use pull_request::PullRequest;
pub use state::RepoState;
pub use status::RepoStatus;
pub use upstream::Upstream;
+18 -4
View File
@@ -21,6 +21,7 @@ use crate::git_store::Mirrors;
use crate::inbox::Inbox;
use crate::push::{PushOutcome, grasp_base_url, grasp_clone_url, push_staged_to_grasps};
use crate::repos::RepoListStore;
use crate::state::GitRef;
pub const USER_KEYRING: &str = "Signed Safe Storage";
pub const NOSTR_CONNECT_TIMEOUT: u64 = 60;
@@ -581,7 +582,10 @@ impl Backend {
&repo_id,
&owner,
&servers,
vec![("refs/heads/main".to_owned(), commit)],
vec![GitRef {
name: "refs/heads/main".to_owned(),
commit: commit_sha,
}],
Some("main".to_owned()),
&destination,
cx,
@@ -668,6 +672,12 @@ impl Backend {
let euc = euc.and_then(|commit| Sha1Hash::from_str(&commit).ok());
let ann = build_announcement(&repo_id, &name, &description, &owner, &servers, euc);
let refs = state
.refs
.iter()
.map(|(name, commit)| GitRef::new(name, commit))
.collect::<Result<Vec<_>, Error>>()?;
let event = announce_repository_and_push(
&client,
&signer,
@@ -675,7 +685,7 @@ impl Backend {
&repo_id,
&owner,
&servers,
state.refs.clone(),
refs,
state.head.clone(),
&path,
cx,
@@ -793,7 +803,11 @@ impl Backend {
}
// Grasp servers authorize a push by the state event they hold in purgatory.
let refs = state.refs.clone();
let refs = state
.refs
.iter()
.map(|(name, commit)| GitRef::new(name, commit))
.collect::<Result<Vec<_>, Error>>()?;
let head = state.head.clone();
let outcome = if refs.is_empty() {
@@ -1196,7 +1210,7 @@ async fn announce_repository_and_push<S>(
repo_id: &str,
owner: &str,
servers: &[RelayUrl],
refs: Vec<(String, String)>,
refs: Vec<GitRef>,
head: Option<String>,
path: &Path,
cx: &mut AsyncApp,
+2
View File
@@ -10,6 +10,7 @@ mod push;
mod refresh;
mod repo;
mod repos;
mod state;
mod sync_status;
use std::path::{Path, PathBuf};
@@ -29,6 +30,7 @@ pub use repo::RepoStore;
pub use repos::RepoListStore;
pub use signed_git::{Nip34Binding, Nip34Kind};
use signed_nostr::NostrBackend;
pub use state::{GitRef, SyncState};
pub use sync_status::{CheckoutSyncStatus, SyncStatusStore};
pub fn init(
+4 -3
View File
@@ -3,7 +3,8 @@ use std::path::Path;
use anyhow::Error;
use nostr::prelude::Url;
use nostr_sdk::prelude::*;
use signed_core::RepoState;
use crate::state::{GitRef, SyncState};
/// `ws://` grasp servers, like ngit, use `http(s)://<host>`.
pub(crate) fn grasp_base_url(relay: &RelayUrl) -> Option<String> {
@@ -133,7 +134,7 @@ pub(crate) async fn push_staged_to_grasps<S>(
client: &Client,
signer: &S,
repo_id: &str,
refs: &[(String, String)],
refs: &[GitRef],
head: Option<&str>,
path: &Path,
owner: &str,
@@ -153,7 +154,7 @@ where
continue;
};
let event = match RepoState::build(repo_id, refs, head)
let event = match SyncState::build(repo_id, refs, head)
.finalize_async(signer)
.await
{
+4 -4
View File
@@ -8,8 +8,7 @@ use nostr::event::IntoEventBuilder;
use nostr_sdk::prelude::*;
use settings::{EventFetchingStrategy, SettingsStore};
use signed_core::{
Announcement, Deletions, Filters, GitEvent, PullRequest, RepoAddr, RepoState, RepoStatus,
filters,
Announcement, Deletions, Filters, GitEvent, PullRequest, RepoAddr, RepoStatus, filters,
};
use signed_git::{Nip34Binding, Repo};
@@ -19,6 +18,7 @@ use crate::checkouts::CheckoutsStore;
use crate::patch_series::{MAX_PATCH_EVENT_BYTES, PatchSeries};
use crate::push::{PushOutcome, grasp_base_url, grasp06_prs_url, pr_clone_urls};
use crate::repos::RepoListStore;
use crate::state::{GitRef, SyncState};
use crate::sync_status::SyncStatusStore;
pub struct RepoStore {
@@ -34,7 +34,7 @@ pub struct RepoStore {
pub loaded: bool,
pub head: Option<String>,
// Refs and timestamp of the latest state event, for sync detection.
pub state_refs: Vec<(String, String)>,
pub state_refs: Vec<GitRef>,
pub state_at: Option<Timestamp>,
pub issues: Vec<Event>,
pub patches: Vec<Event>,
@@ -354,7 +354,7 @@ impl RepoStore {
let all_states = states.into_iter().filter(|e| !deletions.is_deleted(e));
let state =
utils::latest(all_states).map(|event| (RepoState::parse(&event), event.created_at));
utils::latest(all_states).map(|event| (SyncState::parse(&event), event.created_at));
let (mut issues, mut patches, mut pull_requests, mut statuses, mut comments) =
(Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new());
@@ -1,17 +1,41 @@
use std::str::FromStr;
use anyhow::{Error, anyhow};
use bitcoin_hashes::sha1::Hash as Sha1Hash;
use nostr::prelude::*;
/// A git ref with the commit it points at.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RepoState {
pub refs: Vec<(String, String)>,
pub struct GitRef {
/// Full ref name, like `refs/heads/main` or `refs/tags/v1.0`.
pub name: String,
pub commit: Sha1Hash,
}
impl GitRef {
pub fn new(name: impl Into<String>, commit: &str) -> Result<Self, Error> {
let commit = Sha1Hash::from_str(commit).map_err(|_| anyhow!("invalid commit: {commit}"))?;
Ok(Self {
name: name.into(),
commit,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SyncState {
pub refs: Vec<GitRef>,
pub head: Option<String>,
}
impl RepoState {
pub fn build(id: &str, refs: &[(String, String)], head: Option<&str>) -> EventBuilder {
impl SyncState {
pub fn build(id: &str, refs: &[GitRef], head: Option<&str>) -> EventBuilder {
let mut tags: Vec<Tag> = vec![Tag::identifier(id.to_owned())];
for (name, commit) in refs {
tags.push(Tag::parse([name.as_str(), commit.as_str()]).expect("valid ref tag"));
for git_ref in refs {
let commit = git_ref.commit.to_string();
tags.push(Tag::parse([git_ref.name.as_str(), commit.as_str()]).expect("valid ref tag"));
}
if let Some(head) = head {
@@ -31,10 +55,16 @@ impl RepoState {
match Nip34Tag::parse(tag.as_slice()) {
Ok(Nip34Tag::Head(branch)) => head = Some(branch),
Ok(Nip34Tag::RefHead { branch, commit }) => {
refs.push((format!("refs/heads/{branch}"), commit.to_string()));
refs.push(GitRef {
name: format!("refs/heads/{branch}"),
commit,
});
}
Ok(Nip34Tag::RefTag { name, commit }) => {
refs.push((format!("refs/tags/{name}"), commit.to_string()));
refs.push(GitRef {
name: format!("refs/tags/{name}"),
commit,
});
}
_ => {}
}
@@ -51,6 +81,10 @@ mod tests {
const COMMIT_A: &str = "aa231c4c6a5777dc89b42207b499891a344add5c";
const COMMIT_B: &str = "59429cfc6cb35b0a1ddace73b5a5c5ed57b8f5ca";
fn git_ref(name: &str, commit: &str) -> GitRef {
GitRef::new(name, commit).expect("valid ref")
}
fn keys() -> Keys {
Keys::new(
SecretKey::from_hex("0000000000000000000000000000000000000000000000000000000000000001")
@@ -79,35 +113,35 @@ mod tests {
&["refs/tags/v1.0", COMMIT_A],
]);
let state = RepoState::parse(&event);
let state = SyncState::parse(&event);
assert_eq!(state.head.as_deref(), Some("main"));
assert_eq!(
state.refs,
vec![
("refs/heads/main".to_owned(), COMMIT_A.to_owned()),
("refs/heads/dev".to_owned(), COMMIT_B.to_owned()),
("refs/tags/v1.0".to_owned(), COMMIT_A.to_owned()),
git_ref("refs/heads/main", COMMIT_A),
git_ref("refs/heads/dev", COMMIT_B),
git_ref("refs/tags/v1.0", COMMIT_A),
]
);
}
#[test]
fn build_round_trips_through_parse() {
let refs = [
("refs/heads/main".to_owned(), COMMIT_A.to_owned()),
("refs/heads/dev".to_owned(), COMMIT_B.to_owned()),
("refs/tags/v1.0".to_owned(), COMMIT_A.to_owned()),
let refs = vec![
git_ref("refs/heads/main", COMMIT_A),
git_ref("refs/heads/dev", COMMIT_B),
git_ref("refs/tags/v1.0", COMMIT_A),
];
let event = RepoState::build("my-repo", &refs, Some("main"))
let event = SyncState::build("my-repo", &refs, Some("main"))
.finalize(&keys())
.expect("signed event");
assert_eq!(event.kind, Kind::RepoState);
assert_eq!(event.tags.identifier().as_deref(), Some("my-repo"));
let state = RepoState::parse(&event);
let state = SyncState::parse(&event);
assert_eq!(state.refs, refs);
assert_eq!(state.head.as_deref(), Some("main"));
}
+10 -4
View File
@@ -5,13 +5,14 @@ use std::time::Duration;
use anyhow::Error;
use gpui::{App, AppContext, Context, Entity, Global, Subscription};
use nostr_sdk::prelude::*;
use signed_core::{Deletions, Filters, RepoAddr, RepoState};
use signed_core::{Deletions, Filters, RepoAddr};
use signed_git::{Repo, RepoSyncStatus};
use crate::backend::{Backend, BackendEvent};
use crate::checkouts::CheckoutsStore;
use crate::refresh::{RefreshGate, RefreshRequest};
use crate::repos::RepoListStore;
use crate::state::{GitRef, SyncState};
const SYNC_POLL: Duration = Duration::from_secs(180);
const MAX_SYNC_CHECKOUTS: usize = 8;
@@ -167,7 +168,7 @@ impl SyncStatusStore {
let deletion_events = db.query(Filters::deletions()).await?;
let deletions = Deletions::from_events(deletion_events);
let mut states: HashMap<RepoAddr, Vec<(String, String)>> = HashMap::new();
let mut states: HashMap<RepoAddr, Vec<GitRef>> = HashMap::new();
for (addr, _) in &targets {
let events = db.query(addr.state_filter()).await?;
let latest = utils::latest(
@@ -178,7 +179,7 @@ impl SyncStatusStore {
let Some(event) = latest else {
continue;
};
states.insert(addr.clone(), RepoState::parse(&event).refs);
states.insert(addr.clone(), SyncState::parse(&event).refs);
}
let mut statuses: HashMap<RepoAddr, Vec<CheckoutSyncStatus>> = HashMap::new();
@@ -187,10 +188,15 @@ impl SyncStatusStore {
continue;
};
let remote_refs: Vec<(String, String)> = nostr_refs
.iter()
.map(|git_ref| (git_ref.name.clone(), git_ref.commit.to_string()))
.collect();
let mut list = Vec::new();
for path in paths.iter().take(MAX_SYNC_CHECKOUTS) {
let synced =
Repo::try_open(path).and_then(|repo| repo.sync_status(nostr_refs).ok());
Repo::try_open(path).and_then(|repo| repo.sync_status(&remote_refs).ok());
if let Some(status) = synced {
list.push(CheckoutSyncStatus {
+9 -1
View File
@@ -46,8 +46,16 @@ pub enum GitPushMode {
Main,
}
/// A git ref with the commit it points at.
pub struct GitRef {
/// Full ref name, like `refs/heads/main` or `refs/tags/v1.0`.
pub name: String,
/// Commit id, `bitcoin_hashes::sha1::Hash`, the type NIP-34 tags carry.
pub commit: Sha1Hash,
}
pub struct SyncState {
pub refs: Vec<(String, String)>,
pub refs: Vec<GitRef>,
pub head: Option<String>,
/// Local repository to push from; `None` for states parsed from events.
pub path: Option<PathBuf>,