src/commands
src/commands/connect.rs
use crate::{ElvscArgs, RemoteOrLocal, check_output};
use std::collections::HashSet;
use jj_lib::{
config::ConfigValue, ref_name::RemoteNameBuf, repo::Repo, str_util::StringExpression,
};
use jj_cli::{
cli_util::CommandHelper,
command_error::{CommandError, cli_error_with_message, user_error, user_error_with_message},
ui::Ui,
};
use toml_edit::Array;
const NUSHELL_GET_HUB_ENTRIES: &str = r#"
| where { |it| $it.type == dir }
| update name { |it| $it.name | path basename }
| get name
| to json
"#;
pub fn get_hub_entries(hub: &RemoteOrLocal<'_>) -> Result<HashSet<String>, CommandError> {
let path = hub.path().display().to_string();
let script = format!("ls {path} {NUSHELL_GET_HUB_ENTRIES}");
let output = check_output(hub.shell_command(&script).output()?)?;
serde_json::from_slice(&output.stdout)
.map_err(|e| user_error_with_message("Couldn't parse hub entries json", e))
}
pub fn create_git_repository(
ui: &mut Ui,
hub: &RemoteOrLocal<'_>,
repo: &str,
) -> Result<(), CommandError> {
let target = hub.path().join(repo);
let script = format!("git init {} --bare", target.display());
let output = check_output(hub.shell_command(&script).output()?)?;
write!(
ui.stdout_formatter(),
"[{}]: {}",
hub.to_string(),
String::from_utf8_lossy(&output.stdout)
)?;
Ok(())
}
pub async fn run(
ui: &mut Ui,
ch: &CommandHelper,
hub: RemoteOrLocal<'_>,
public: &str,
output: RemoteOrLocal<'_>,
repo: Option<&str>,
reconnect: bool,
dry_run: bool,
) -> Result<(), CommandError> {
if ElvscArgs::from_config(ch).is_ok() && !reconnect {
return Err(user_error(
"Repository is already connected to a hub. Use '--reconnect' to replace the existing connection",
));
}
let mut wch = ch.workspace_helper(ui)?;
let repo = match repo {
Some(s) => s.to_owned(),
None => wch
.workspace_root()
.file_name()
.and_then(|os| os.to_str())
.map(str::to_owned)
.ok_or_else(|| {
user_error("No repository name was passed and none could be deduced from workspace")
})?,
};
let hub_str = hub.to_string();
let output_str = output.to_string();
let git_url = hub.join(&repo).to_string();
let git_repo = jj_lib::git::get_git_repo(wch.repo().store())?;
let (origin_taken, should_add_git_remote) =
if let Some(Ok(remote)) = git_repo.try_find_remote("origin") {
let push = remote
.url(gix::remote::Direction::Push)
.map(|url| url.to_bstring())
.unwrap_or_else(|| "<no URL>".into());
let fetch = remote
.url(gix::remote::Direction::Fetch)
.map(|url| url.to_bstring())
.unwrap_or_else(|| "<no URL>".into());
if push != fetch {
(true, true)
} else {
if push == git_url {
(true, false)
} else {
(true, true)
}
}
} else {
(false, true)
};
let remote_name: String = if origin_taken && should_add_git_remote {
let answer =
ui.prompt(&format!("Repository {} origin already exists as a git remote. Enter a name for the elvsc remote", repo))?;
let trimmed = answer.trim();
if trimmed.is_empty() {
return Err(user_error("Remote name cannot be empty"));
}
if git_repo.try_find_remote(trimmed).is_some() {
return Err(user_error(format!(
"Remote '{trimmed}' already exists too — aborting"
)));
}
trimmed.to_owned()
} else {
"origin".to_owned()
};
{
let mut stdout = ui.stdout_formatter();
if dry_run {
writeln!(
stdout,
"[dry-run] no changes will be made to the filesystem or repository"
)?;
}
writeln!(stdout, "Connecting to hub:")?;
writeln!(stdout, " repo: {repo}")?;
writeln!(stdout, " hub: {hub_str}")?;
writeln!(stdout, " public: {public}")?;
writeln!(stdout, " output: {output_str}")?;
writeln!(stdout, " remote: {remote_name}")?;
writeln!(stdout, " git url: {git_url}")?;
if reconnect {
writeln!(stdout, " (replacing existing connection)")?;
}
}
let entries = get_hub_entries(&hub)?;
let repo_exists = entries.contains(&repo);
if repo_exists {
writeln!(
ui.stdout_formatter(),
"✓ hub already has a bare repository named '{repo}'"
)?;
} else if dry_run {
writeln!(
ui.stdout_formatter(),
"[dry-run] would create bare repository: '{}/{repo}'",
hub.to_string()
)?;
} else {
writeln!(
ui.stdout_formatter(),
"→ creating bare repository on hub..."
)?;
create_git_repository(ui, &hub, &repo)?;
}
let config_entries: Vec<(&str, ConfigValue)> = vec![
("elvsc.repo", repo.as_str().into()),
("elvsc.hub", hub_str.as_str().into()),
("elvsc.public", public.into()),
("elvsc.output", output_str.as_str().into()),
("elvsc.remote", remote_name.as_str().into()),
(
"aliases.export",
Array::from_iter([
"--elvsc-update",
"git",
"push",
"--remote",
remote_name.as_str(),
"--all",
])
.into(),
),
(
"aliases.import",
Array::from_iter(["git", "fetch", "--remote", remote_name.as_str()]).into(),
),
];
if dry_run {
writeln!(
ui.stdout_formatter(),
"[dry-run] would write to repo config:"
)?;
for (key, value) in &config_entries {
writeln!(ui.stdout_formatter(), " {key} = {value}")?;
}
} else {
let mut files = ch.config_env().repo_config_files(ui, ch.raw_config())?;
let config = files
.get_mut(0)
.ok_or_else(|| user_error("No repo config path found to edit"))?;
for (key, value) in &config_entries {
config
.set_value(*key, value)
.map_err(|e| cli_error_with_message("Couldn't set value in config file", e))?;
}
config
.save()
.map_err(|e| cli_error_with_message("Couldn't save config file", e))?;
writeln!(
ui.stdout_formatter(),
"✓ wrote {} config entries",
config_entries.len()
)?;
}
if should_add_git_remote && dry_run {
writeln!(
ui.stdout_formatter(),
"[dry-run] would add git remote '{remote_name}' → {git_url}"
)?;
} else if should_add_git_remote {
let mut tx = wch.start_transaction();
jj_lib::git::add_remote(
tx.repo_mut(),
&RemoteNameBuf::from(remote_name.as_str()),
&git_url,
None,
gix::remote::fetch::Tags::All,
&StringExpression::all(),
)?;
tx.finish(ui, format!("add git remote {remote_name}"))
.await?;
writeln!(
ui.stdout_formatter(),
"✓ added git remote '{remote_name}' → {git_url}"
)?;
}
if dry_run {
writeln!(
ui.stdout_formatter(),
"[dry-run] complete — no changes made"
)?;
} else {
writeln!(ui.stdout_formatter(), "✓ connection complete")?;
}
Ok(())
}