Let the git remote be set from Settings

Attaching a workspace to an existing repository meant leaving for a
terminal: gitsync could init, commit and push, and could tell whether an
'origin' existed, but had no way to create one, and nothing in the
settings held a remote URL. Sync just logged "no 'origin' remote
configured" and committed locally.

Settings > Git remote... now shows the workspace repository's origin and
lets it be set, repointed or cleared. The URL is read from and written
to the repository, never to config.json, because a remote belongs to the
checkout — so each workspace shows its own, and the box is refreshed
whenever the repository in play changes.

This is separate from the Gitea publishing in New project, which is a
cookiecutter post-gen hook and only ever applies to generated projects.

Also factors out first_output_line, which repo_root and the new
origin_url were both open-coding, and origin_args, so the add-vs-set-url
decision can be tested without a repository on disk.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBWj9TphFMCoh7VHaSRnvQ
This commit is contained in:
landon
2026-09-09 17:54:27 -05:00
parent aa050cf411
commit 91718f7487
5 changed files with 246 additions and 9 deletions
+12
View File
@@ -203,6 +203,12 @@ pub struct App {
rename_focus: bool,
/// Whether the delete confirmation opened from that menu is up.
confirm_delete: bool,
/// Editable copy of the workspace repository's `origin` URL. Read back from
/// git whenever the repository changes, never persisted to the config —
/// the remote belongs to the checkout, not to the app.
remote_input: String,
/// Whether the git-remote dialog is up.
show_git_remote: bool,
status: String,
show_log: bool,
git_log: String,
@@ -371,6 +377,8 @@ impl App {
show_rename: false,
rename_focus: false,
confirm_delete: false,
remote_input: String::new(),
show_git_remote: false,
status: String::new(),
show_log: false,
git_log: String::new(),
@@ -534,6 +542,10 @@ impl eframe::App for App {
self.confirm_delete_window(ctx);
}
if self.show_git_remote {
self.git_remote_window(ctx);
}
if self.show_new_project {
self.new_project_window(ctx);
}
+4
View File
@@ -424,6 +424,10 @@ impl App {
ui.close_menu();
self.show_project_settings = true;
}
if ui.button("Git remote…").clicked() {
ui.close_menu();
self.open_git_remote();
}
if ui.button("Manuscript details…").clicked() {
ui.close_menu();
self.show_manuscript_settings = true;
+110
View File
@@ -137,6 +137,7 @@ impl App {
if ws.join(".git").exists() {
self.is_repo = true;
self.repo_root = Some(ws.to_path_buf());
self.remote_input = gitsync::origin_url(ws).unwrap_or_default();
return;
}
match gitsync::repo_root(ws) {
@@ -151,10 +152,12 @@ impl App {
Some(root) => {
self.is_repo = true;
self.repo_root = Some(root);
self.remote_input = gitsync::origin_url(ws).unwrap_or_default();
}
None => {
self.is_repo = false;
self.repo_root = None;
self.remote_input.clear();
}
}
}
@@ -166,6 +169,7 @@ impl App {
self.status = format!("Using git repository at {}", root.display());
self.is_repo = true;
self.repo_root = Some(root);
self.remote_input = gitsync::origin_url(self.workspace()).unwrap_or_default();
}
}
@@ -660,6 +664,7 @@ impl App {
self.git_log = outcome.log;
self.show_log = true;
self.is_repo = gitsync::is_repo(&ws);
self.remote_input = gitsync::origin_url(&ws).unwrap_or_default();
self.repo_root = self.is_repo.then_some(ws);
self.status = if self.is_repo {
"Initialised git repository".to_string()
@@ -1025,6 +1030,111 @@ impl App {
}
/// Settings dialog for the markdown seeded into template-backed new files.
/// Open the git-remote dialog, reading the current `origin` fresh so the
/// 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();
self.show_git_remote = true;
}
/// 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
/// repository and read straight back, so what the box shows afterwards is
/// what git holds, and a different project shows its own.
pub(super) fn set_git_remote(&mut self) {
let ws = self.workspace().to_path_buf();
if !self.is_repo {
self.status = "Not a git repository yet \u{2014} use \u{201c}Init git\u{201d} first"
.to_string();
return;
}
let outcome = gitsync::set_origin(&ws, &self.remote_input);
self.git_log = outcome.log;
self.show_log = true;
self.remote_input = gitsync::origin_url(&ws).unwrap_or_default();
self.status = if !outcome.ok {
"Could not set the remote (see log)".to_string()
} else if self.remote_input.is_empty() {
"Removed the origin remote".to_string()
} else {
format!("origin is now {}", self.remote_input)
};
}
/// The dialog for attaching a workspace to an existing remote repository.
///
/// Creating the repository on the server is a template hook's job, and only
/// happens for projects made through New project. Anything else — a
/// workspace that predates the app, or one whose hook did not run — needs
/// the remote set by hand, which otherwise meant leaving for a terminal.
pub(super) fn git_remote_window(&mut self, ctx: &egui::Context) {
let mut open = self.show_git_remote;
let mut apply = false;
let mut close = false;
let is_repo = self.is_repo;
let root = self
.repo_root
.as_ref()
.map(|r| r.display().to_string())
.unwrap_or_default();
egui::Window::new("Git remote")
.open(&mut open)
.resizable(false)
.collapsible(false)
.default_width(460.0)
.show(ctx, |ui| {
if !is_repo {
ui.label(
egui::RichText::new(
"This workspace is not a git repository yet. Use \u{201c}Init \
git\u{201d} in the toolbar first, then set the remote here.",
)
.small()
.weak(),
);
ui.separator();
}
ui.label(egui::RichText::new(format!("Repository: {root}")).small().weak());
ui.add_space(4.0);
ui.label("origin:");
let r = ui.add(
egui::TextEdit::singleline(&mut self.remote_input)
.desired_width(f32::INFINITY)
.hint_text("ssh://git@host:port/user/repo.git"),
);
apply |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
ui.label(
egui::RichText::new(
"The repository has to exist on the server already \u{2014} this \
attaches to it, it does not create it. Until origin is set, \
\u{201c}\u{27f3} Sync (git)\u{201d} commits locally and says so in \
the log. Clear the box and press Set to detach.",
)
.small()
.weak(),
);
ui.add_space(4.0);
ui.label(
egui::RichText::new(
"Stored in the repository, not in this app's settings, so each \
project keeps its own.",
)
.small()
.weak(),
);
ui.separator();
ui.horizontal(|ui| {
apply |= ui.add_enabled(is_repo, egui::Button::new("Set origin")).clicked();
close |= ui.button("Close").clicked();
});
});
if apply {
self.set_git_remote();
}
self.show_git_remote = open && !close;
}
/// Carry out what a file row's right-click menu picked.
///
/// Right-clicking acts on the row you clicked, so that row becomes the