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:
+102
-6
@@ -27,6 +27,17 @@ fn run_git(workspace: &Path, args: &[&str]) -> (bool, String) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The first real output line of a `run_git` result.
|
||||
///
|
||||
/// `run_git` prefixes a `$ git …` echo for the log window, so a caller after a
|
||||
/// single value — a path, a URL — has to step over it.
|
||||
fn first_output_line(out: &str) -> Option<&str> {
|
||||
out.lines()
|
||||
.find(|l| !l.starts_with("$ git") && !l.trim().is_empty())
|
||||
.map(str::trim)
|
||||
.filter(|l| !l.is_empty())
|
||||
}
|
||||
|
||||
/// True if the workspace directory is inside a git working tree.
|
||||
pub fn is_repo(workspace: &Path) -> bool {
|
||||
let (ok, out) = run_git(workspace, &["rev-parse", "--is-inside-work-tree"]);
|
||||
@@ -43,12 +54,7 @@ pub fn repo_root(workspace: &Path) -> Option<PathBuf> {
|
||||
if !ok {
|
||||
return None;
|
||||
}
|
||||
// run_git prepends a `$ git …` echo line; the toplevel path is the first
|
||||
// real output line.
|
||||
let line = out
|
||||
.lines()
|
||||
.find(|l| !l.starts_with("$ git") && !l.trim().is_empty())?;
|
||||
let path = PathBuf::from(line.trim());
|
||||
let path = PathBuf::from(first_output_line(&out)?);
|
||||
(!path.as_os_str().is_empty()).then_some(path)
|
||||
}
|
||||
|
||||
@@ -58,6 +64,49 @@ fn has_origin(workspace: &Path) -> bool {
|
||||
ok && out.lines().any(|l| l.trim() == "origin")
|
||||
}
|
||||
|
||||
/// The URL `origin` points at, if the remote exists.
|
||||
///
|
||||
/// Read from the repository rather than from this app's settings: a remote
|
||||
/// belongs to the checkout, so the workspace is the source of truth and opening
|
||||
/// a different project shows that project's remote.
|
||||
pub fn origin_url(workspace: &Path) -> Option<String> {
|
||||
let (ok, out) = run_git(workspace, &["remote", "get-url", "origin"]);
|
||||
if !ok {
|
||||
return None;
|
||||
}
|
||||
first_output_line(&out).map(str::to_string)
|
||||
}
|
||||
|
||||
/// Point `origin` at `url`, adding the remote when it is not there yet.
|
||||
///
|
||||
/// A blank `url` removes the remote instead, which is how a workspace is
|
||||
/// disconnected from a server without touching its history.
|
||||
pub fn set_origin(workspace: &Path, url: &str) -> GitOutcome {
|
||||
let Some(args) = origin_args(url, has_origin(workspace)) else {
|
||||
return GitOutcome {
|
||||
ok: true,
|
||||
log: "(there was no 'origin' to remove)\n".to_string(),
|
||||
};
|
||||
};
|
||||
let (ok, log) = run_git(workspace, &args);
|
||||
GitOutcome { ok, log }
|
||||
}
|
||||
|
||||
/// The git command that points `origin` at `url`, or `None` when there is
|
||||
/// nothing to do. Kept separate from running it, as with the cookiecutter
|
||||
/// argument builder, so the decision table can be asserted in tests without a
|
||||
/// repository on disk — picking `add` where `set-url` was needed (or the other
|
||||
/// way round) fails at the git level, not here.
|
||||
pub fn origin_args(url: &str, has_origin: bool) -> Option<Vec<&str>> {
|
||||
let url = url.trim();
|
||||
match (url.is_empty(), has_origin) {
|
||||
(true, true) => Some(vec!["remote", "remove", "origin"]),
|
||||
(true, false) => None,
|
||||
(false, true) => Some(vec!["remote", "set-url", "origin", url]),
|
||||
(false, false) => Some(vec!["remote", "add", "origin", url]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialise a new git repository in the workspace with a starter .gitignore.
|
||||
pub fn init(workspace: &Path) -> GitOutcome {
|
||||
let mut log = String::new();
|
||||
@@ -154,3 +203,50 @@ pub fn has_commits(workspace: &Path) -> bool {
|
||||
fn strip_command_echo(text: &str) -> String {
|
||||
text.split_once('\n').map_or(text, |(_, rest)| rest).to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn setting_a_remote_adds_it_only_when_it_is_missing() {
|
||||
// Fresh repository: the remote has to be created.
|
||||
assert_eq!(
|
||||
origin_args("ssh://git@host:2228/me/book.git", false),
|
||||
Some(vec!["remote", "add", "origin", "ssh://git@host:2228/me/book.git"])
|
||||
);
|
||||
// Already pointed somewhere: `add` would fail, so repoint instead.
|
||||
assert_eq!(
|
||||
origin_args("ssh://git@host:2228/me/book.git", true),
|
||||
Some(vec!["remote", "set-url", "origin", "ssh://git@host:2228/me/book.git"])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clearing_the_box_detaches_the_remote() {
|
||||
assert_eq!(origin_args("", true), Some(vec!["remote", "remove", "origin"]));
|
||||
assert_eq!(origin_args(" ", true), Some(vec!["remote", "remove", "origin"]));
|
||||
// Nothing to remove, so nothing to run — and no error to report.
|
||||
assert_eq!(origin_args("", false), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pasted_url_is_trimmed_before_it_reaches_git() {
|
||||
// Copying a clone URL out of a web page brings whitespace with it, and
|
||||
// git would take it literally.
|
||||
assert_eq!(
|
||||
origin_args(" ssh://git@host/me/book.git\n", false),
|
||||
Some(vec!["remote", "add", "origin", "ssh://git@host/me/book.git"])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_command_echo_is_skipped_when_reading_a_single_value() {
|
||||
// What `run_git` actually hands back: its own echo line, then output.
|
||||
let out = "$ git remote get-url origin\nssh://git@host/me/book.git\n";
|
||||
assert_eq!(first_output_line(out), Some("ssh://git@host/me/book.git"));
|
||||
// No remote configured: git says nothing, so neither do we.
|
||||
assert_eq!(first_output_line("$ git remote get-url origin\n"), None);
|
||||
assert_eq!(first_output_line(""), None);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user