// Copyright 2020-2023 The Jujutsu Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use gix::Url; use gix::remote::Direction; use jj_lib::git; use jj_lib::ref_name::RemoteName; use jj_lib::ref_name::RemoteNameBuf; use jj_lib::repo::Repo; use crate::cli_util::CommandHelper; use crate::command_error::CommandError; use crate::git_util::absolute_git_url; use crate::ui::Ui; /// Add a Git remote #[derive(clap::Args, Clone, Debug)] pub struct GitRemoteAddArgs { /// The remote's name remote: RemoteNameBuf, /// The remote's URL or path /// /// Local path will be resolved to absolute form. #[arg(value_hint = clap::ValueHint::Url)] url: String, /// The URL used for push /// /// Local path will be resolved to absolute form #[arg(long, value_hint = clap::ValueHint::Url)] push_url: Option, } pub async fn cmd_git_remote_add( ui: &mut Ui, command: &CommandHelper, args: &GitRemoteAddArgs, ) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui).await?; let url = absolute_git_url(command.cwd(), &args.url)?; let push_url = args .push_url .as_deref() .map(|url| absolute_git_url(command.cwd(), url)) .transpose()?; let mut tx = workspace_command.start_transaction(); git::add_remote(tx.repo_mut(), &args.remote, &url, push_url.as_deref())?; warn_if_remote_url_matches(ui, tx.repo(), &args.remote, &url, push_url.as_deref())?; tx.finish(ui, format!("add git remote {}", args.remote.as_symbol())) .await?; Ok(()) } /// Warns if the new configured URL exactly matches an existing configured URL. /// /// URL equivalence is not reliable. For example, HTTPS, SSH, .git suffixes, and /// redirects can identify the same repository. Git URL rewrite rules can also /// make different configured strings resolve to the same destination, so this /// is only an exact-match warning. A remote without a configured push URL is /// treated as pushing to its fetch URL, matching Git's behavior. fn warn_if_remote_url_matches( ui: &Ui, repo: &dyn Repo, new_remote: &RemoteName, new_fetch_url: &str, new_push_url: Option<&str>, ) -> Result<(), CommandError> { let git_repo = git::get_git_repo(repo.store())?; // Git uses the fetch URL for push when no push URL is configured. let new_push_url = new_push_url.unwrap_or(new_fetch_url); let mut warned = false; for remote_name in git_repo.remote_names() { // Ignore the remote that was just added. if remote_name == new_remote.as_str() { continue; } // Ignore empty or unloadable remote sections for this advisory warning. let Some(Ok(remote)) = git_repo.try_find_remote_without_url_rewrite(&**remote_name) else { continue; }; let remote_fetch_url = remote.url(Direction::Fetch).map(Url::to_bstring); let remote_push_url = remote.url(Direction::Push).map(Url::to_bstring); let remote_url_matches = [remote_fetch_url, remote_push_url] .iter() .flatten() .any(|remote_url| remote_url == new_fetch_url || remote_url == new_push_url); // Don't print the URL itself because remote URLs can contain credentials, // such as user:password or token path segments. if remote_url_matches { writeln!( ui.warning_default(), "Remote {remote_name} already uses the same URL." )?; warned = true; } } if warned { writeln!( ui.hint_default(), "If this was a mistake, run `jj git remote remove {new_remote}`.", new_remote = new_remote.as_symbol() )?; } Ok(()) }