diff --git a/CHANGELOG.md b/CHANGELOG.md index fa54822c71..6b186d07a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `jj prev` and `jj next` have gained a `--conflict` flag which moves you to the next conflict in a child commit. +* New command `jj git remote set-url` that sets the url of a git remote. + ### Fixed bugs ## [0.18.0] - 2024-06-05 diff --git a/cli/src/commands/git/remote/mod.rs b/cli/src/commands/git/remote/mod.rs index 3026803ddc..ff634656f1 100644 --- a/cli/src/commands/git/remote/mod.rs +++ b/cli/src/commands/git/remote/mod.rs @@ -16,6 +16,7 @@ pub mod add; pub mod list; pub mod remove; pub mod rename; +pub mod set_url; use clap::Subcommand; @@ -23,6 +24,7 @@ use self::add::{cmd_remote_add, AddArgs}; use self::list::{cmd_remote_list, ListArgs}; use self::remove::{cmd_remote_remove, RemoveArgs}; use self::rename::{cmd_remote_rename, RenameArgs}; +use self::set_url::{cmd_remote_set_url, SetUrlArgs}; use crate::cli_util::CommandHelper; use crate::command_error::CommandError; use crate::ui::Ui; @@ -36,6 +38,7 @@ pub enum RemoteCommand { List(ListArgs), Remove(RemoveArgs), Rename(RenameArgs), + SetUrl(SetUrlArgs), } pub fn cmd_git_remote( @@ -48,5 +51,6 @@ pub fn cmd_git_remote( RemoteCommand::List(args) => cmd_remote_list(ui, command, args), RemoteCommand::Remove(args) => cmd_remote_remove(ui, command, args), RemoteCommand::Rename(args) => cmd_remote_rename(ui, command, args), + RemoteCommand::SetUrl(args) => cmd_remote_set_url(ui, command, args), } } diff --git a/cli/src/commands/git/remote/set_url.rs b/cli/src/commands/git/remote/set_url.rs new file mode 100644 index 0000000000..f3e29ce184 --- /dev/null +++ b/cli/src/commands/git/remote/set_url.rs @@ -0,0 +1,42 @@ +// Copyright 2024 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 jj_lib::git; +use jj_lib::repo::Repo; + +use crate::cli_util::CommandHelper; +use crate::command_error::CommandError; +use crate::git_util::get_git_repo; +use crate::ui::Ui; + +/// Set the URL of a Git remote +#[derive(clap::Args, Clone, Debug)] +pub struct SetUrlArgs { + /// The remote's name + remote: String, + /// The desired url for `remote` + url: String, +} + +pub fn cmd_remote_set_url( + ui: &mut Ui, + command: &CommandHelper, + args: &SetUrlArgs, +) -> Result<(), CommandError> { + let workspace_command = command.workspace_helper(ui)?; + let repo = workspace_command.repo(); + let git_repo = get_git_repo(repo.store())?; + git::set_remote_url(&git_repo, &args.remote, &args.url)?; + Ok(()) +} diff --git a/cli/tests/cli-reference@.md.snap b/cli/tests/cli-reference@.md.snap index 50e35bd139..431229e56a 100644 --- a/cli/tests/cli-reference@.md.snap +++ b/cli/tests/cli-reference@.md.snap @@ -52,6 +52,7 @@ This document contains the help content for the `jj` command-line program. * [`jj git remote list`↴](#jj-git-remote-list) * [`jj git remote remove`↴](#jj-git-remote-remove) * [`jj git remote rename`↴](#jj-git-remote-rename) +* [`jj git remote set-url`↴](#jj-git-remote-set-url) * [`jj init`↴](#jj-init) * [`jj interdiff`↴](#jj-interdiff) * [`jj log`↴](#jj-log) @@ -955,6 +956,7 @@ The Git repo will be a bare git repo stored inside the `.jj/` directory. * `list` — List Git remotes * `remove` — Remove a Git remote and forget its branches * `rename` — Rename a Git remote +* `set-url` — Set the URL of a Git remote @@ -1004,6 +1006,19 @@ Rename a Git remote +## `jj git remote set-url` + +Set the URL of a Git remote + +**Usage:** `jj git remote set-url ` + +###### **Arguments:** + +* `` — The remote's name +* `` — The desired url for `remote` + + + ## `jj init` Create a new repo in the given directory diff --git a/cli/tests/test_git_remotes.rs b/cli/tests/test_git_remotes.rs index 5f6194fdae..9987a5e1b1 100644 --- a/cli/tests/test_git_remotes.rs +++ b/cli/tests/test_git_remotes.rs @@ -90,6 +90,60 @@ fn test_git_remote_add() { "###); } +#[test] +fn test_git_remote_set_url() { + let test_env = TestEnvironment::default(); + + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); + let repo_path = test_env.env_root().join("repo"); + test_env.jj_cmd_ok( + &repo_path, + &["git", "remote", "add", "foo", "http://example.com/repo/foo"], + ); + let stderr = test_env.jj_cmd_failure( + &repo_path, + &[ + "git", + "remote", + "set-url", + "bar", + "http://example.com/repo/bar", + ], + ); + insta::assert_snapshot!(stderr, @r###" + Error: No git remote named 'bar' + "###); + let stderr = test_env.jj_cmd_failure( + &repo_path, + &[ + "git", + "remote", + "set-url", + "git", + "http://example.com/repo/git", + ], + ); + insta::assert_snapshot!(stderr, @r###" + Error: Git remote named 'git' is reserved for local Git repository + "###); + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &[ + "git", + "remote", + "set-url", + "foo", + "http://example.com/repo/bar", + ], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @""); + let stdout = test_env.jj_cmd_success(&repo_path, &["git", "remote", "list"]); + insta::assert_snapshot!(stdout, @r###" + foo http://example.com/repo/bar + "###); +} + #[test] fn test_git_remote_rename() { let test_env = TestEnvironment::default(); diff --git a/lib/src/git.rs b/lib/src/git.rs index fb3b709166..3086d1c34b 100644 --- a/lib/src/git.rs +++ b/lib/src/git.rs @@ -1144,6 +1144,32 @@ pub fn rename_remote( Ok(()) } +pub fn set_remote_url( + git_repo: &git2::Repository, + remote_name: &str, + new_remote_url: &str, +) -> Result<(), GitRemoteManagementError> { + if remote_name == REMOTE_NAME_FOR_LOCAL_GIT_REPO { + return Err(GitRemoteManagementError::RemoteReservedForLocalGitRepo); + } + + // Repository::remote_set_url() doesn't ensure the remote exists, it just + // creates it if it's missing. + // Therefore ensure it exists first + git_repo.find_remote(remote_name).map_err(|err| { + if is_remote_not_found_err(&err) { + GitRemoteManagementError::NoSuchRemote(remote_name.to_owned()) + } else { + GitRemoteManagementError::InternalGitError(err) + } + })?; + + git_repo + .remote_set_url(remote_name, new_remote_url) + .map_err(GitRemoteManagementError::InternalGitError)?; + Ok(()) +} + fn rename_remote_refs(mut_repo: &mut MutableRepo, old_remote_name: &str, new_remote_name: &str) { mut_repo.rename_remote(old_remote_name, new_remote_name); let prefix = format!("refs/remotes/{old_remote_name}/");