Skip to content

Commit

Permalink
completion: teach git about remote urls
Browse files Browse the repository at this point in the history
  • Loading branch information
senekor committed Nov 16, 2024
1 parent 77477ae commit e4c39c4
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 2 deletions.
4 changes: 3 additions & 1 deletion cli/src/commands/git/clone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use std::num::NonZeroU32;
use std::path::Path;
use std::path::PathBuf;

use clap_complete::ArgValueCompleter;
use jj_lib::git;
use jj_lib::git::GitFetchError;
use jj_lib::git::GitFetchStats;
Expand All @@ -33,6 +34,7 @@ use crate::command_error::user_error;
use crate::command_error::user_error_with_message;
use crate::command_error::CommandError;
use crate::commands::git::maybe_add_gitignore;
use crate::complete;
use crate::config::write_config_value_to_file;
use crate::config::ConfigNamePathBuf;
use crate::git_util::get_git_repo;
Expand All @@ -47,7 +49,7 @@ use crate::ui::Ui;
#[derive(clap::Args, Clone, Debug)]
pub struct GitCloneArgs {
/// URL or path of the Git repo to clone
#[arg(value_hint = clap::ValueHint::DirPath)]
#[arg(add = ArgValueCompleter::new(complete::new_git_remote_url))]
source: String,
/// Specifies the target directory for the Jujutsu repository clone.
/// If not provided, defaults to a directory named after the last component
Expand Down
3 changes: 3 additions & 0 deletions cli/src/commands/git/remote/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use clap_complete::ArgValueCompleter;
use jj_lib::git;
use jj_lib::repo::Repo;

use crate::cli_util::CommandHelper;
use crate::command_error::CommandError;
use crate::complete;
use crate::git_util::get_git_repo;
use crate::ui::Ui;

Expand All @@ -26,6 +28,7 @@ pub struct GitRemoteAddArgs {
/// The remote's name
remote: String,
/// The remote's URL
#[arg(add = ArgValueCompleter::new(complete::new_git_remote_url))]
url: String,
}

Expand Down
3 changes: 2 additions & 1 deletion cli/src/commands/git/remote/set_url.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use clap_complete::ArgValueCandidates;
use clap_complete::{ArgValueCandidates, ArgValueCompleter};
use jj_lib::git;
use jj_lib::repo::Repo;

Expand All @@ -29,6 +29,7 @@ pub struct GitRemoteSetUrlArgs {
#[arg(add = ArgValueCandidates::new(complete::git_remotes))]
remote: String,
/// The desired url for `remote`
#[arg(add = ArgValueCompleter::new(complete::new_git_remote_url))]
url: String,
}

Expand Down
56 changes: 56 additions & 0 deletions cli/src/complete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,62 @@ pub fn git_remotes() -> Vec<CompletionCandidate> {
})
}

pub fn new_git_remote_url(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
let Some(current) = current.to_str() else {
return Vec::new();
};

let known_schemas = ["git@", "https://", "http://"];

let schema_candiates = known_schemas
.iter()
.filter(|&&schema| schema.starts_with(current) && !current.starts_with(schema))
.map(CompletionCandidate::new)
.collect_vec();
if !schema_candiates.is_empty() {
return schema_candiates;
}

let Some(&schema) = known_schemas.iter().find(|&s| current.starts_with(s)) else {
// We don't know the schema the user is attempting to use.
return Vec::new();
};
let current = current.strip_prefix(schema).unwrap();

let domain_terminator = match schema {
"git@" => ":",
_ => "/", // http, probably
};

if current.contains(domain_terminator) {
// Completing the path after the domain as well would be cool, but it's
// more difficult. One approach could be to cache the URLs that were
// cloned previously. Users probably clone repositories from the same
// user / organisations frequently.
return Vec::new();
}

let Some(known_hosts) = dirs::home_dir()
.and_then(|home| std::fs::read_to_string(home.join(".ssh/known_hosts")).ok())
else {
return Vec::new(); // cannot determine domain candidates
};

let mut candidates = Vec::new();
for line in known_hosts.lines() {
let domain = line.split_whitespace().next().unwrap_or(line);
if !domain.starts_with(current) {
continue;
}
let display_order = if domain.contains("git") { 0 } else { 1 };
candidates.push(
CompletionCandidate::new(format!("{schema}{domain}{domain_terminator}"))
.display_order(Some(display_order)),
);
}
candidates
}

pub fn aliases() -> Vec<CompletionCandidate> {
with_jj(|_, config| {
Ok(config
Expand Down
77 changes: 77 additions & 0 deletions cli/tests/test_completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -400,3 +400,80 @@ fn test_operations() {
let stdout = test_env.jj_cmd_success(&repo_path, &["--", "jj", "op", "undo", "5b"]);
insta::assert_snapshot!(stdout, @"5bbb4ca536a8 (2001-02-03 08:05:12) describe commit 968261075dddabf4b0e333c1cc9a49ce26a3f710");
}

#[test]
fn test_new_git_remote() {
let test_env = TestEnvironment::default();

let ssh_dir = test_env.home_dir().join(".ssh");
std::fs::create_dir(&ssh_dir).unwrap();
std::fs::write(
ssh_dir.join("known_hosts"),
"\
random-server.com long-fingerprint
self-hosted-git-server.com long-fingerprint
company-git-server.com long-fingerprint
",
)
.unwrap();

let mut test_env = test_env;
test_env.add_env_var("COMPLETE", "fish");
let test_env = test_env;

let stdout = test_env.jj_cmd_success(test_env.env_root(), &["--", "jj", "git", "clone", ""]);
insta::assert_snapshot!(stdout, @r"
git@
https://
http://
--remote Name of the newly created remote
--colocate Whether or not to colocate the Jujutsu repo with the git repo
--depth Create a shallow clone of the given depth
--help Print help (see more with '--help')
--repository Path to repository to operate on
--ignore-working-copy Don't snapshot the working copy, and don't update it
--ignore-immutable Allow rewriting immutable commits
--at-operation Operation to load the repo at
--debug Enable debug logging
--color When to colorize output (always, never, debug, auto)
--quiet Silence non-primary command output
--no-pager Disable the pager
--config-toml Additional configuration options (can be repeated)
");

let stdout = test_env.jj_cmd_success(test_env.env_root(), &["--", "jj", "git", "clone", "gi"]);
insta::assert_snapshot!(stdout, @"git@");
let stdout = test_env.jj_cmd_success(test_env.env_root(), &["--", "jj", "git", "clone", "ht"]);
insta::assert_snapshot!(stdout, @r"
https://
http://
");

let stdout =
test_env.jj_cmd_success(test_env.env_root(), &["--", "jj", "git", "clone", "git@"]);
insta::assert_snapshot!(stdout, @r"
[email protected]:
[email protected]:
[email protected]:
");
let stdout = test_env.jj_cmd_success(
test_env.env_root(),
&["--", "jj", "git", "clone", "https://"],
);
insta::assert_snapshot!(stdout, @r"
https://self-hosted-git-server.com/
https://company-git-server.com/
https://random-server.com/
");

let stdout = test_env.jj_cmd_success(
test_env.env_root(),
&["--", "jj", "git", "clone", "git@sel"],
);
insta::assert_snapshot!(stdout, @"[email protected]:");
let stdout = test_env.jj_cmd_success(
test_env.env_root(),
&["--", "jj", "git", "clone", "https://com"],
);
insta::assert_snapshot!(stdout, @"https://company-git-server.com/");
}

0 comments on commit e4c39c4

Please sign in to comment.