Create the Gitea repository from the app, not a template hook
Repository creation was delegated entirely to a cookiecutter template's post-gen hook. That only ever covered projects made through New project, and when it went wrong it went wrong inside someone else's Python, where all this app could report was that a hook exited non-zero. The app now talks to Gitea itself. Settings > Git remote... gains a "Create and attach" button: it POSTs to /api/v1/user/repos with the configured server, user and token, then points origin at the clone URL that comes back. A name that is already taken is fetched and attached to rather than raised as an error, so retrying after a failure works. The call runs on a worker thread with the existing channel-and-poll pattern, so the UI does not block on the network. Errors are mapped by status code, because a rejected token, a URL that is not a Gitea API and a name the server refuses each need a different fix. Uses ureq's send_string rather than send_json, whose feature this build does not enable; no new dependency, and ldd still shows only libc/libgcc/libm. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GBWj9TphFMCoh7VHaSRnvQ
This commit is contained in:
@@ -209,6 +209,13 @@ pub struct App {
|
||||
remote_input: String,
|
||||
/// Whether the git-remote dialog is up.
|
||||
show_git_remote: bool,
|
||||
/// Name to give the repository the Gitea section would create.
|
||||
gitea_repo_name: String,
|
||||
/// Result channel for the in-flight Gitea call, if there is one. Its
|
||||
/// presence is also what greys the button out, so only one can run.
|
||||
gitea_rx: Option<std::sync::mpsc::Receiver<Result<crate::gitea::Repo, String>>>,
|
||||
/// What the Gitea section is saying about the last (or running) attempt.
|
||||
gitea_status: String,
|
||||
status: String,
|
||||
show_log: bool,
|
||||
git_log: String,
|
||||
@@ -379,6 +386,9 @@ impl App {
|
||||
confirm_delete: false,
|
||||
remote_input: String::new(),
|
||||
show_git_remote: false,
|
||||
gitea_repo_name: String::new(),
|
||||
gitea_rx: None,
|
||||
gitea_status: String::new(),
|
||||
status: String::new(),
|
||||
show_log: false,
|
||||
git_log: String::new(),
|
||||
@@ -545,6 +555,7 @@ impl eframe::App for App {
|
||||
if self.show_git_remote {
|
||||
self.git_remote_window(ctx);
|
||||
}
|
||||
self.poll_gitea_create();
|
||||
|
||||
if self.show_new_project {
|
||||
self.new_project_window(ctx);
|
||||
|
||||
+7
-5
@@ -506,7 +506,7 @@ impl App {
|
||||
}
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.label(egui::RichText::new("Gitea (used by the template's hook)").strong());
|
||||
ui.label(egui::RichText::new("Gitea").strong());
|
||||
egui::Grid::new("gitea_grid")
|
||||
.num_columns(2)
|
||||
.spacing([10.0, 8.0])
|
||||
@@ -539,10 +539,12 @@ impl App {
|
||||
});
|
||||
ui.label(
|
||||
egui::RichText::new(
|
||||
"Passed to the hook as GITEA_URL / GITEA_USER / GITEA_TOKEN. \
|
||||
Leave blank and the hook skips publishing — the project is \
|
||||
still created. The token is stored in this app's config file \
|
||||
in plain text.",
|
||||
"Used two ways: the app creates repositories with them from \
|
||||
Settings ▸ Git remote, and passes them to a template's hooks \
|
||||
as GITEA_URL / GITEA_USER / GITEA_TOKEN. Leave them blank and \
|
||||
both simply skip — the project is still created. The token \
|
||||
needs write access to repositories, and is stored in this \
|
||||
app's config file in plain text.",
|
||||
)
|
||||
.small()
|
||||
.weak(),
|
||||
|
||||
+133
-2
@@ -1034,9 +1034,70 @@ impl App {
|
||||
/// box shows what the repository actually holds.
|
||||
pub(super) fn open_git_remote(&mut self) {
|
||||
self.remote_input = gitsync::origin_url(self.workspace()).unwrap_or_default();
|
||||
if self.gitea_repo_name.trim().is_empty() {
|
||||
// The project folder's name is nearly always what the repository
|
||||
// should be called, and it is the tedious part to type.
|
||||
self.gitea_repo_name = self
|
||||
.project_root()
|
||||
.file_name()
|
||||
.map(|n| crate::gitea::normalize_name(&n.to_string_lossy()))
|
||||
.unwrap_or_default();
|
||||
}
|
||||
self.gitea_status.clear();
|
||||
self.show_git_remote = true;
|
||||
}
|
||||
|
||||
/// Ask Gitea for the repository named in the dialog, on a worker thread.
|
||||
///
|
||||
/// The call goes over the network, so it cannot happen on the UI thread;
|
||||
/// [`App::poll_gitea_create`] picks the answer up. While one is in flight
|
||||
/// `gitea_rx` is `Some`, which is also what greys the button out.
|
||||
fn start_gitea_create(&mut self, ctx: &egui::Context) {
|
||||
if self.gitea_rx.is_some() {
|
||||
return;
|
||||
}
|
||||
let server = self.config.gitea_url.clone();
|
||||
let user = self.config.gitea_user.clone();
|
||||
let token = self.config.gitea_token.clone();
|
||||
let repo = crate::gitea::NewRepo {
|
||||
name: self.gitea_repo_name.clone(),
|
||||
description: String::new(),
|
||||
private: self.config.gitea_private,
|
||||
};
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
self.gitea_rx = Some(rx);
|
||||
self.gitea_status = "Asking Gitea\u{2026}".to_string();
|
||||
let ctx = ctx.clone();
|
||||
std::thread::spawn(move || {
|
||||
let _ = tx.send(crate::gitea::create(&server, &user, &token, &repo));
|
||||
ctx.request_repaint();
|
||||
});
|
||||
}
|
||||
|
||||
/// Pick up a finished Gitea call and point `origin` at what came back.
|
||||
///
|
||||
/// Creating the repository and attaching to it are one action as far as the
|
||||
/// reader is concerned, so the remote is set here rather than left as a
|
||||
/// second button to remember. Pushing is still [`App::git_sync`]'s job —
|
||||
/// this only makes it possible.
|
||||
pub(super) fn poll_gitea_create(&mut self) {
|
||||
let received = self.gitea_rx.as_ref().and_then(|rx| rx.try_recv().ok());
|
||||
let Some(result) = received else { return };
|
||||
self.gitea_rx = None;
|
||||
match result {
|
||||
Ok(repo) => {
|
||||
self.remote_input = repo.remote_url(self.config.gitea_use_ssh).to_string();
|
||||
self.set_git_remote();
|
||||
let verb = if repo.created { "Created" } else { "Already had" };
|
||||
self.gitea_status = format!(
|
||||
"\u{2714} {verb} {} \u{2014} origin set. Press \u{27f3} Sync (git) to push.",
|
||||
repo.full_name
|
||||
);
|
||||
}
|
||||
Err(e) => self.gitea_status = format!("\u{2716} {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Point the workspace's repository at the typed remote.
|
||||
///
|
||||
/// Nothing about the URL is kept in this app's config: it is written to the
|
||||
@@ -1072,7 +1133,14 @@ impl App {
|
||||
let mut open = self.show_git_remote;
|
||||
let mut apply = false;
|
||||
let mut close = false;
|
||||
let mut create = false;
|
||||
let mut save_now = false;
|
||||
let is_repo = self.is_repo;
|
||||
let busy = self.gitea_rx.is_some();
|
||||
// Read out before the closure borrows `self` mutably to draw with.
|
||||
let server = self.config.gitea_url.trim().to_string();
|
||||
let user = self.config.gitea_user.trim().to_string();
|
||||
let have_token = !self.config.gitea_token.trim().is_empty();
|
||||
let root = self
|
||||
.repo_root
|
||||
.as_ref()
|
||||
@@ -1123,15 +1191,78 @@ impl App {
|
||||
.small()
|
||||
.weak(),
|
||||
);
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
apply |= ui.add_enabled(is_repo, egui::Button::new("Set origin")).clicked();
|
||||
close |= ui.button("Close").clicked();
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
ui.label(egui::RichText::new("Create it on Gitea").strong());
|
||||
if server.is_empty() || !have_token {
|
||||
ui.label(
|
||||
egui::RichText::new(
|
||||
"Set the Gitea server, user and token in Settings \u{25b8} New \
|
||||
project first \u{2014} the app uses them for this too.",
|
||||
)
|
||||
.small()
|
||||
.weak(),
|
||||
);
|
||||
} else {
|
||||
ui.label(
|
||||
egui::RichText::new(format!("{server} as {user}"))
|
||||
.small()
|
||||
.weak(),
|
||||
);
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Name:");
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut self.gitea_repo_name)
|
||||
.desired_width(200.0)
|
||||
.hint_text("my-book"),
|
||||
);
|
||||
save_now |= ui.checkbox(&mut self.config.gitea_private, "Private").changed();
|
||||
});
|
||||
save_now |= ui
|
||||
.checkbox(&mut self.config.gitea_use_ssh, "Attach over SSH")
|
||||
.on_hover_text(
|
||||
"SSH pushes on a key. Turn this off to use the HTTPS clone \
|
||||
URL, which asks for a credential on every push.",
|
||||
)
|
||||
.changed();
|
||||
ui.add_space(4.0);
|
||||
ui.horizontal(|ui| {
|
||||
let ready = is_repo && !server.is_empty() && have_token && !busy;
|
||||
create |= ui
|
||||
.add_enabled(ready, egui::Button::new("Create and attach"))
|
||||
.on_hover_text(
|
||||
"Create the repository on Gitea and point origin at it. \
|
||||
A name that already exists is attached to rather than \
|
||||
treated as an error.",
|
||||
)
|
||||
.clicked();
|
||||
if busy {
|
||||
ui.spinner();
|
||||
}
|
||||
});
|
||||
if !self.gitea_status.is_empty() {
|
||||
ui.label(egui::RichText::new(&self.gitea_status).small());
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
if ui.button("Close").clicked() {
|
||||
close = true;
|
||||
}
|
||||
if save_now {
|
||||
self.config.save();
|
||||
}
|
||||
});
|
||||
if apply {
|
||||
self.set_git_remote();
|
||||
}
|
||||
if create {
|
||||
self.start_gitea_create(ctx);
|
||||
}
|
||||
self.show_git_remote = open && !close;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user