create local repo

This commit is contained in:
2026-09-03 13:59:54 +07:00
parent 01f0540726
commit c0f06d095e
4 changed files with 155 additions and 15 deletions
+55 -10
View File
@@ -5,7 +5,7 @@ use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::time::{Duration, Instant};
use anyhow::{Error, anyhow, bail};
use anyhow::{Context as AnyhowContext, Error, anyhow, bail};
use bitcoin_hashes::sha1::Hash as Sha1Hash;
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Task};
use nostr::event::IntoEventBuilder;
@@ -390,7 +390,10 @@ impl Backend {
/// Create a new repository: initialize a local clone with a `main`
/// branch and a `README.md`, publish the NIP-34 announcement and the
/// repository state to the grasp relays, then push the initial commit
/// to each grasp server.
/// to each grasp server. A working copy of the repository is also
/// created at `<folder>/<name>` (named like the repo header's Clone
/// action), with `origin` pointing at the first grasp server, so the
/// new project exists in the chosen folder right away.
///
/// The events must reach the grasp servers *before* the push: GRASP
/// servers hold the signed state event in "purgatory" and only accept
@@ -398,23 +401,30 @@ impl Backend {
/// pending (it expires after 30 minutes), like gitworkshop and ngit.
///
/// The git work runs on background threads; the task yields the
/// published announcement.
/// published announcement and the path of the created working copy.
pub fn create_repository(
&mut self,
name: &str,
description: &str,
folder: PathBuf,
grasp_servers: Vec<RelayUrl>,
cx: &mut Context<Self>,
) -> Task<Result<Announcement, Error>> {
) -> Task<Result<(Announcement, PathBuf), Error>> {
let name = name.trim().to_owned();
let description = description.trim().to_owned();
if name.is_empty() {
return Task::ready(Err(anyhow!("Repository name is required")));
}
if grasp_servers.is_empty() {
return Task::ready(Err(anyhow!("Add at least one grasp server")));
}
if !folder.is_dir() {
return Task::ready(Err(anyhow!("Choose a folder for the repository")));
}
let Some(public_key) = self.current_user else {
return Task::ready(Err(anyhow!("Sign in to create a repository")));
};
@@ -443,9 +453,10 @@ impl Backend {
let servers = grasp_servers.clone();
cx.spawn(async move |this, cx| {
// Initialize the local clone (main branch + README + initial commit).
// Initialize the local clone and create the user's working copy from it.
let work = cx.background_spawn({
let path = path.clone();
let folder = folder.clone();
let name = name.clone();
let description = description.clone();
let owner = owner.clone();
@@ -466,13 +477,43 @@ impl Backend {
signed_git::ensure_origin(&path, &url).ok();
}
Ok::<_, Error>(commit)
// A working copy at `<folder>/<name>` (the same naming
// as the header's Clone action), cloned from the mirror
// above so it shares the announced history exactly;
// `origin` is re-pointed at the first grasp server
// instead of the mirror path.
let destination = {
let dir_name = signed_git::sanitize_path_component(&name);
let dir_name = if dir_name.is_empty() {
"repository".to_owned()
} else {
dir_name
};
folder.join(dir_name)
};
let mirror_url = Url::from_file_path(&path)
.map_err(|_| anyhow!("invalid mirror path"))?
.to_string();
signed_git::clone_repo(&[mirror_url], &destination).with_context(|| {
format!(
"failed to create the working copy at {}",
destination.display()
)
})?;
if let Some(base) = servers.first().and_then(grasp_base_url) {
let url = format!("{base}/{owner}/{repo_id}.git");
signed_git::set_origin(&destination, &url)?;
}
Ok::<_, Error>((commit, destination))
}
});
let commit = work.await?;
let commit_sha =
Sha1Hash::from_str(&commit).map_err(|_| anyhow!("invalid initial commit id"))?;
let (commit, checkout_path) = work.await?;
let commit_sha = Sha1Hash::from_str(&commit).map_err(|_| anyhow!("invalid id"))?;
// The nostr client queues events until each relay is connected.
this.update(cx, |this, cx| {
@@ -536,6 +577,7 @@ impl Backend {
let servers = servers.clone();
push_to_grasp_servers(path, owner, repo_id, servers, signed_git::push_main)
});
if let Err(e) = push.await {
// The events are already published; retract them so the
// repository doesn't remain announced without content.
@@ -550,7 +592,10 @@ impl Backend {
));
}
Announcement::from_event(&event).ok_or_else(|| anyhow!("failed to parse announcement"))
let announcement = Announcement::from_event(&event)
.ok_or_else(|| anyhow!("failed to parse announcement"))?;
Ok((announcement, checkout_path))
})
}