feat: multi-steps push progress #30
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>,
|
||||
|
||||
Reference in New Issue
Block a user