feat: allow choosing the default branch when publishing a repo
Rust / build (macos-latest, stable) (push) Waiting to run
Rust / build (ubuntu-latest, stable) (push) Waiting to run
Rust / build (windows-latest, stable) (push) Waiting to run

This commit is contained in:
2026-10-07 11:52:53 +07:00
parent 5887a14c50
commit 5586f67f0e
3 changed files with 117 additions and 38 deletions
+30 -24
View File
@@ -515,6 +515,7 @@ impl Backend {
let owner = public_key.to_bech32().unwrap(); let owner = public_key.to_bech32().unwrap();
let servers = grasp_servers.clone(); let servers = grasp_servers.clone();
let client = self.client.clone(); let client = self.client.clone();
let signer = self.signer.clone();
let destination = { let destination = {
let dir_name = GitCache::sanitize_path_component(&name); let dir_name = GitCache::sanitize_path_component(&name);
@@ -554,9 +555,7 @@ impl Backend {
let commit = work.await?; let commit = work.await?;
let commit_sha = Sha1Hash::from_str(&commit).map_err(|_| anyhow!("invalid id"))?; let commit_sha = Sha1Hash::from_str(&commit).map_err(|_| anyhow!("invalid id"))?;
let signer = this.update(cx, |this, _cx| this.signer.clone())?; let announcement = build_announcement(
let announcement = repository_announcement(
&repo_id, &repo_id,
&name, &name,
&description, &description,
@@ -594,6 +593,7 @@ impl Backend {
name: &str, name: &str,
description: &str, description: &str,
grasp_servers: Vec<RelayUrl>, grasp_servers: Vec<RelayUrl>,
default_branch: Option<String>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Task<Result<Announcement, Error>> { ) -> Task<Result<Announcement, Error>> {
let name = name.trim().to_owned(); let name = name.trim().to_owned();
@@ -627,10 +627,13 @@ impl Backend {
let owner = public_key.to_bech32().unwrap(); let owner = public_key.to_bech32().unwrap();
let servers = grasp_servers.clone(); let servers = grasp_servers.clone();
let client = self.client.clone(); let client = self.client.clone();
let signer = self.signer.clone();
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
let work = cx.background_spawn({ let (mut state, euc) = cx
.background_spawn({
let path = path.clone(); let path = path.clone();
async move { async move {
let repo = Repo::open(&path)?; let repo = Repo::open(&path)?;
@@ -638,20 +641,29 @@ impl Backend {
let euc = repo.root_commit()?; let euc = repo.root_commit()?;
Ok::<_, Error>((state, euc)) Ok::<_, Error>((state, euc))
} }
}); })
let (state, euc) = work.await?; .await?;
let signer = this.update(cx, |this, _cx| this.signer.clone())?; let chosen = default_branch
.as_deref()
.map(str::trim)
.filter(|branch| !branch.is_empty());
if let Some(branch) = chosen {
let wanted = format!("refs/heads/{branch}");
if state.refs.iter().any(|(name, _)| name == &wanted) {
state.head = Some(branch.to_owned());
}
}
let euc = euc.and_then(|commit| Sha1Hash::from_str(&commit).ok()); let euc = euc.and_then(|commit| Sha1Hash::from_str(&commit).ok());
let announcement = let ann = build_announcement(&repo_id, &name, &description, &owner, &servers, euc);
repository_announcement(&repo_id, &name, &description, &owner, &servers, euc);
let event = announce_repository_and_push( let event = announce_repository_and_push(
&this, &this,
&client, &client,
&signer, &signer,
announcement, ann,
&repo_id, &repo_id,
&owner, &owner,
&servers, &servers,
@@ -667,9 +679,9 @@ impl Backend {
let url = format!("{base}/{owner}/{repo_id}.git"); let url = format!("{base}/{owner}/{repo_id}.git");
let path = path.clone(); let path = path.clone();
cx.background_spawn(async move { cx.background_spawn(async move {
Repo::open(&path) if let Err(e) = Repo::open(&path).and_then(|r| r.ensure_origin(&url)) {
.and_then(|repo| repo.ensure_origin(&url)) log::warn!("failed to ensure origin: {e}");
.ok(); }
}) })
.await; .await;
} }
@@ -681,13 +693,8 @@ impl Backend {
Ok(naddr) => { Ok(naddr) => {
let path = path.clone(); let path = path.clone();
cx.background_spawn(async move { cx.background_spawn(async move {
if let Err(error) = if let Err(e) = Repo::open(&path).and_then(|r| r.set_nostr_repo(&naddr)) {
Repo::open(&path).and_then(|repo| repo.set_nostr_repo(&naddr)) log::warn!("failed to record the NIP-34 marker: {e}");
{
log::warn!(
"failed to record the NIP-34 marker for {}: {error}",
path.display()
);
} }
}) })
.await; .await;
@@ -742,6 +749,8 @@ impl Backend {
let owner = announcement.owner.to_bech32().unwrap(); let owner = announcement.owner.to_bech32().unwrap();
let repo_id = announcement.id.clone(); let repo_id = announcement.id.clone();
let relays = announcement.relays.clone(); let relays = announcement.relays.clone();
let client = self.client.clone();
let signer = self.signer.clone();
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
let _guard = cx.on_drop(&this, { let _guard = cx.on_drop(&this, {
@@ -779,9 +788,6 @@ impl Backend {
let refs = state.refs.clone(); let refs = state.refs.clone();
let head = state.head.clone(); let head = state.head.clone();
let (client, signer) =
this.update(cx, |this, _cx| (this.client.clone(), this.signer.clone()))?;
let outcome = if refs.is_empty() { let outcome = if refs.is_empty() {
PushOutcome::default() PushOutcome::default()
} else { } else {
@@ -1145,7 +1151,7 @@ impl Backend {
} }
} }
fn repository_announcement( fn build_announcement(
repo_id: &str, repo_id: &str,
name: &str, name: &str,
description: &str, description: &str,
+73 -2
View File
@@ -2,16 +2,19 @@ use std::path::PathBuf;
use assets::CustomIconName; use assets::CustomIconName;
use gpui::prelude::*; use gpui::prelude::*;
use gpui::{App, Entity, WeakEntity, Window, px}; use gpui::{App, Entity, SharedString, WeakEntity, Window, px};
use gpui_base::h_flex; use gpui_base::h_flex;
use gpui_base::input::TextareaState; use gpui_base::input::TextareaState;
use gpui_component::button::{Button, ButtonVariants}; use gpui_component::button::{Button, ButtonVariants};
use gpui_component::dialog::{DialogDescription, DialogFooter, DialogHeader, DialogTitle}; use gpui_component::dialog::{DialogDescription, DialogFooter, DialogHeader, DialogTitle};
use gpui_component::form::{field, v_form}; use gpui_component::form::{field, v_form};
use gpui_component::input::{Input, InputState, Textarea}; use gpui_component::input::{Input, InputState, Textarea};
use gpui_component::select::{Select, SelectState};
use gpui_component::{ActiveTheme, Disableable, WindowExt}; use gpui_component::{ActiveTheme, Disableable, WindowExt};
use settings::SettingsStore; use settings::SettingsStore;
use signed_git::Repo;
use signed_state::Backend; use signed_state::Backend;
use signed_ui::SelectOption;
use super::RepoDetailView; use super::RepoDetailView;
use crate::views::dialog_state::{DialogProgress, error_row}; use crate::views::dialog_state::{DialogProgress, error_row};
@@ -48,8 +51,11 @@ pub fn open(
.auto_grow(3, 5) .auto_grow(3, 5)
.placeholder("Short description") .placeholder("Short description")
}); });
let branch_select: Entity<SelectState<Vec<SelectOption>>> =
cx.new(|cx| SelectState::new(Vec::new(), None, window, cx));
load_user_grasp_servers(grasp_state.clone(), window, cx); load_user_grasp_servers(grasp_state.clone(), window, cx);
load_branches(local_path.clone(), branch_select.clone(), window, cx);
window.open_dialog(cx, move |dialog, _window, _cx| { window.open_dialog(cx, move |dialog, _window, _cx| {
const DESC: &str = "Publish this local repository to Nostr."; const DESC: &str = "Publish this local repository to Nostr.";
@@ -57,6 +63,7 @@ pub fn open(
let name_input = name_input.clone(); let name_input = name_input.clone();
let desc_input = desc_input.clone(); let desc_input = desc_input.clone();
let relay_input = relay_input.clone(); let relay_input = relay_input.clone();
let branch_select = branch_select.clone();
let state = state.clone(); let state = state.clone();
let grasp_state = grasp_state.clone(); let grasp_state = grasp_state.clone();
let local_path = local_path.clone(); let local_path = local_path.clone();
@@ -105,6 +112,12 @@ pub fn open(
.child(local_path.display().to_string()), .child(local_path.display().to_string()),
), ),
) )
.child(
field()
.label("Default branch")
.description("The local branch to announce as the default")
.child(Select::new(&branch_select).w_full()),
)
.child(grasp_servers_field(&grasp_state, &relay_input, cx)), .child(grasp_servers_field(&grasp_state, &relay_input, cx)),
) )
.children(error_row(&error, cx)) .children(error_row(&error, cx))
@@ -120,6 +133,7 @@ pub fn open(
.on_click({ .on_click({
let name_input = name_input.clone(); let name_input = name_input.clone();
let desc_input = desc_input.clone(); let desc_input = desc_input.clone();
let branch_select = branch_select.clone();
let state = state.clone(); let state = state.clone();
let grasp_state = grasp_state.clone(); let grasp_state = grasp_state.clone();
let local_path = local_path.clone(); let local_path = local_path.clone();
@@ -129,6 +143,7 @@ pub fn open(
init_repository( init_repository(
local_path.clone(), local_path.clone(),
(name_input.clone(), desc_input.clone()), (name_input.clone(), desc_input.clone()),
branch_select.clone(),
state.clone(), state.clone(),
grasp_state.clone(), grasp_state.clone(),
view.clone(), view.clone(),
@@ -143,9 +158,11 @@ pub fn open(
}); });
} }
#[allow(clippy::too_many_arguments)]
fn init_repository( fn init_repository(
local_path: PathBuf, local_path: PathBuf,
inputs: (Entity<InputState>, Entity<TextareaState>), inputs: (Entity<InputState>, Entity<TextareaState>),
branch_select: Entity<SelectState<Vec<SelectOption>>>,
state: Entity<InitRepoState>, state: Entity<InitRepoState>,
grasp_state: Entity<GraspServersState>, grasp_state: Entity<GraspServersState>,
view: WeakEntity<RepoDetailView>, view: WeakEntity<RepoDetailView>,
@@ -156,6 +173,11 @@ fn init_repository(
let name = name_input.read(cx).value().trim().to_owned(); let name = name_input.read(cx).value().trim().to_owned();
let description = desc_input.read(cx).value().trim().to_owned(); let description = desc_input.read(cx).value().trim().to_owned();
let servers = grasp_state.read(cx).grasp_servers.clone(); let servers = grasp_state.read(cx).grasp_servers.clone();
let default_branch = branch_select
.read(cx)
.selected_value()
.map(|value| value.trim().to_owned())
.filter(|branch| !branch.is_empty());
if name.is_empty() { if name.is_empty() {
state.update(cx, |state, _| state.fail("Repository name is required")); state.update(cx, |state, _| state.fail("Repository name is required"));
@@ -171,7 +193,14 @@ fn init_repository(
let backend = Backend::global(cx); let backend = Backend::global(cx);
let task = backend.update(cx, |backend, cx| { let task = backend.update(cx, |backend, cx| {
backend.publish_local_repo(local_path.clone(), &name, &description, servers, cx) backend.publish_local_repo(
local_path.clone(),
&name,
&description,
servers,
default_branch,
cx,
)
}); });
let handle = window.window_handle(); let handle = window.window_handle();
@@ -199,3 +228,45 @@ fn init_repository(
}) })
.detach(); .detach();
} }
/// Fills the default-branch selector with the repository's local branches,
/// preselecting the checked-out one.
fn load_branches(
local_path: PathBuf,
select: Entity<SelectState<Vec<SelectOption>>>,
window: &mut Window,
cx: &mut App,
) {
let handle = window.window_handle();
cx.spawn(async move |cx| {
let loaded = cx
.background_spawn(async move {
let repo = Repo::open(&local_path)?;
let current: Option<SharedString> = repo.current_branch().map(Into::into);
Ok::<_, anyhow::Error>((repo.branches()?, current))
})
.await;
let _ = cx.update_window(handle, |_, window, cx| {
let Ok((branches, current)) = loaded.map_err(|error| {
log::warn!("failed to list branches for the default branch selector: {error:#}")
}) else {
return;
};
let options: Vec<SelectOption> = branches
.into_iter()
.map(|branch| SelectOption::new(branch.clone(), branch))
.collect();
select.update(cx, |state, cx| {
state.set_items(options, window, cx);
if let Some(current) = current.as_ref() {
state.set_selected_value(current, window, cx);
}
});
});
})
.detach();
}
+7 -5
View File
@@ -828,10 +828,6 @@ impl RepoDetailView {
} }
/// Open the upstream repository, the `u` tag of this fork's announcement. /// Open the upstream repository, the `u` tag of this fork's announcement.
///
/// The announcement may not be in the local database yet. The panel opens
/// from the address and fills in when the store loads it; the store's
/// `subscribe_remote` fetches it from the bootstrap relays.
pub(super) fn open_upstream(&mut self, window: &mut Window, cx: &mut Context<Self>) { pub(super) fn open_upstream(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(addr) = self let Some(addr) = self
.announcement(cx) .announcement(cx)
@@ -1788,6 +1784,12 @@ impl RepoDetailView {
/// The Files tab body, or the clone/initial-load spinner. /// The Files tab body, or the clone/initial-load spinner.
fn render_files_tab(&self, cx: &mut Context<Self>) -> AnyElement { fn render_files_tab(&self, cx: &mut Context<Self>) -> AnyElement {
if self.loading { if self.loading {
let label = if self.store.read(cx).path.is_some() {
"Loading repository..."
} else {
"Cloning repository..."
};
return v_flex() return v_flex()
.flex_1() .flex_1()
.size_full() .size_full()
@@ -1799,7 +1801,7 @@ impl RepoDetailView {
div() div()
.text_sm() .text_sm()
.text_color(cx.theme().muted_foreground) .text_color(cx.theme().muted_foreground)
.child("Cloning repository..."), .child(label),
) )
.into_any_element(); .into_any_element();
} }