// 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 std::collections::HashSet;use std::io::Write;use std::ops::Deref;use std::path::{Path, PathBuf};use std::{fmt, fs, io}; use clap::{ArgGroup, Subcommand};use itertools::Itertools;use jj_lib::backend::TreeValue;use jj_lib::git::{ self, parse_gitmodules, GitBranchPushTargets, GitFetchError, GitFetchStats, GitPushError,};use jj_lib::object_id::ObjectId;use jj_lib::op_store::RefTarget;use jj_lib::refs::{ classify_branch_push_action, BranchPushAction, BranchPushUpdate, TrackingRefPair,};use jj_lib::repo::Repo;use jj_lib::repo_path::RepoPath;use jj_lib::revset::{self, RevsetExpression, RevsetIteratorExt as _};use jj_lib::settings::{ConfigResultExt as _, UserSettings};use jj_lib::str_util::StringPattern;use jj_lib::view::View;use jj_lib::workspace::Workspace;use maplit::hashset; use crate::cli_util::{ parse_string_pattern, resolve_multiple_nonempty_revsets, short_change_hash, short_commit_hash, user_error, user_error_with_hint, CommandError, CommandHelper, RevisionArg, WorkspaceCommandHelper,};use crate::git_util::{ get_git_repo, print_failed_git_export, print_git_import_stats, with_remote_git_callbacks,};use crate::ui::Ui; /// Commands for working with the underlying Git repo////// For a comparison with Git, including a table of commands, see/// https://github.com/martinvonz/jj/blob/main/docs/git-comparison.md.#[derive(Subcommand, Clone, Debug)]pub enum GitCommand { #[command(subcommand)] Remote(GitRemoteCommand), Fetch(GitFetchArgs), Clone(GitCloneArgs), Push(GitPushArgs), Import(GitImportArgs), Export(GitExportArgs), #[command(subcommand, hide = true)] Submodule(GitSubmoduleCommand),} /// Manage Git remotes////// The Git repo will be a bare git repo stored inside the `.jj/` directory.#[derive(Subcommand, Clone, Debug)]pub enum GitRemoteCommand { Add(GitRemoteAddArgs), Remove(GitRemoteRemoveArgs), Rename(GitRemoteRenameArgs), List(GitRemoteListArgs),} /// Add a Git remote#[derive(clap::Args, Clone, Debug)]pub struct GitRemoteAddArgs { /// The remote's name remote: String, /// The remote's URL url: String,} /// Remove a Git remote and forget its branches#[derive(clap::Args, Clone, Debug)]pub struct GitRemoteRemoveArgs { /// The remote's name remote: String,} /// Rename a Git remote#[derive(clap::Args, Clone, Debug)]pub struct GitRemoteRenameArgs { /// The name of an existing remote old: String, /// The desired name for `old` new: String,} /// List Git remotes#[derive(clap::Args, Clone, Debug)]pub struct GitRemoteListArgs {} /// Fetch from a Git remote#[derive(clap::Args, Clone, Debug)]pub struct GitFetchArgs { /// Fetch only some of the branches /// /// By default, the specified name matches exactly. Use `glob:` prefix to /// expand `*` as a glob. The other wildcard characters aren't supported. #[arg(long, default_value = "glob:*", value_parser = parse_string_pattern)] branch: Vec<StringPattern>, /// The remote to fetch from (only named remotes are supported, can be /// repeated) #[arg(long = "remote", value_name = "remote")] remotes: Vec<String>, /// Fetch from all remotes #[arg(long, conflicts_with = "remotes")] all_remotes: bool,} /// Create a new repo backed by a clone of a Git repo////// The Git repo will be a bare git repo stored inside the `.jj/` directory.#[derive(clap::Args, Clone, Debug)]pub struct GitCloneArgs { /// URL or path of the Git repo to clone #[arg(value_hint = clap::ValueHint::DirPath)] source: String, /// The directory to write the Jujutsu repo to #[arg(value_hint = clap::ValueHint::DirPath)] destination: Option<String>, /// Whether or not to colocate the Jujutsu repo with the git repo #[arg(long)] colocate: bool,} /// Push to a Git remote////// By default, pushes any branches pointing to/// `remote_branches(remote=<remote>)..@`. Use `--branch` to push specific/// branches. Use `--all` to push all branches. Use `--change` to generate/// branch names based on the change IDs of specific commits.#[derive(clap::Args, Clone, Debug)]#[command(group(ArgGroup::new("specific").args(&["branch", "change", "revisions"]).multiple(true)))]#[command(group(ArgGroup::new("what").args(&["all", "deleted"]).conflicts_with("specific")))]pub struct GitPushArgs { /// The remote to push to (only named remotes are supported) #[arg(long)] remote: Option<String>, /// Push only this branch (can be repeated) /// /// By default, the specified name matches exactly. Use `glob:` prefix to /// select branches by wildcard pattern. For details, see /// https://github.com/martinvonz/jj/blob/main/docs/revsets.md#string-patterns. #[arg(long, short, value_parser = parse_string_pattern)] branch: Vec<StringPattern>, /// Push all branches (including deleted branches) #[arg(long)] all: bool, /// Push all deleted branches #[arg(long)] deleted: bool, /// Push branches pointing to these commits #[arg(long, short)] revisions: Vec<RevisionArg>, /// Push this commit by creating a branch based on its change ID (can be /// repeated) #[arg(long, short)] change: Vec<RevisionArg>, /// Only display what will change on the remote #[arg(long)] dry_run: bool,} /// Update repo with changes made in the underlying Git repo#[derive(clap::Args, Clone, Debug)]pub struct GitImportArgs {} /// Update the underlying Git repo with changes made in the repo#[derive(clap::Args, Clone, Debug)]pub struct GitExportArgs {} /// FOR INTERNAL USE ONLY Interact with git submodules#[derive(Subcommand, Clone, Debug)]pub enum GitSubmoduleCommand { /// Print the relevant contents from .gitmodules. For debugging purposes /// only. PrintGitmodules(GitSubmodulePrintGitmodulesArgs),} /// Print debugging info about Git submodules#[derive(clap::Args, Clone, Debug)]#[command(hide = true)]pub struct GitSubmodulePrintGitmodulesArgs { /// Read .gitmodules from the given revision. #[arg(long, short = 'r', default_value = "@")] revisions: RevisionArg,} fn make_branch_term(branch_names: &[impl fmt::Display]) -> String { match branch_names { [branch_name] => format!("branch {}", branch_name), branch_names => format!("branches {}", branch_names.iter().join(", ")), }} fn map_git_error(err: git2::Error) -> CommandError { if err.class() == git2::ErrorClass::Ssh { let hint = if err.code() == git2::ErrorCode::Certificate && std::env::var_os("HOME").is_none() { "The HOME environment variable is not set, and might be required for Git to \ successfully load certificates. Try setting it to the path of a directory that \ contains a `.ssh` directory." } else { "Jujutsu uses libssh2, which doesn't respect ~/.ssh/config. Does `ssh -F \ /dev/null` to the host work?" }; user_error_with_hint(err.to_string(), hint) } else { user_error(err.to_string()) }} pub fn maybe_add_gitignore(workspace_command: &WorkspaceCommandHelper) -> Result<(), CommandError> { if workspace_command.working_copy_shared_with_git() { std::fs::write( workspace_command .workspace_root() .join(".jj") .join(".gitignore"), "/*\n", ) .map_err(|e| user_error(format!("Failed to write .jj/.gitignore file: {e}"))) } else { Ok(()) }} fn cmd_git_remote_add( ui: &mut Ui, command: &CommandHelper, args: &GitRemoteAddArgs,) -> Result<(), CommandError> { let workspace_command = command.workspace_helper(ui)?; let repo = workspace_command.repo(); let git_repo = get_git_repo(repo.store())?; git::add_remote(&git_repo, &args.remote, &args.url)?; Ok(())} fn cmd_git_remote_remove( ui: &mut Ui, command: &CommandHelper, args: &GitRemoteRemoveArgs,) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; let repo = workspace_command.repo(); let git_repo = get_git_repo(repo.store())?; let mut tx = workspace_command.start_transaction(); git::remove_remote(tx.mut_repo(), &git_repo, &args.remote)?; if tx.mut_repo().has_changes() { tx.finish(ui, format!("remove git remote {}", &args.remote)) } else { Ok(()) // Do not print "Nothing changed." }} fn cmd_git_remote_rename( ui: &mut Ui, command: &CommandHelper, args: &GitRemoteRenameArgs,) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; let repo = workspace_command.repo(); let git_repo = get_git_repo(repo.store())?; let mut tx = workspace_command.start_transaction(); git::rename_remote(tx.mut_repo(), &git_repo, &args.old, &args.new)?; if tx.mut_repo().has_changes() { tx.finish( ui, format!("rename git remote {} to {}", &args.old, &args.new), ) } else { Ok(()) // Do not print "Nothing changed." }} fn cmd_git_remote_list( ui: &mut Ui, command: &CommandHelper, _args: &GitRemoteListArgs,) -> Result<(), CommandError> { let workspace_command = command.workspace_helper(ui)?; let repo = workspace_command.repo(); let git_repo = get_git_repo(repo.store())?; for remote_name in git_repo.remotes()?.iter().flatten() { let remote = git_repo.find_remote(remote_name)?; writeln!( ui.stdout(), "{} {}", remote_name, remote.url().unwrap_or("<no URL>") )?; } Ok(())} #[tracing::instrument(skip(ui, command))]fn cmd_git_fetch( ui: &mut Ui, command: &CommandHelper, args: &GitFetchArgs,) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; let git_repo = get_git_repo(workspace_command.repo().store())?; let remotes = if args.all_remotes { get_all_remotes(&git_repo)? } else if args.remotes.is_empty() { get_default_fetch_remotes(ui, command.settings(), &git_repo)? } else { args.remotes.clone() }; let mut tx = workspace_command.start_transaction(); for remote in &remotes { let stats = with_remote_git_callbacks(ui, |cb| { git::fetch( tx.mut_repo(), &git_repo, remote, &args.branch, cb, &command.settings().git_settings(), ) }) .map_err(|err| match err { GitFetchError::InvalidBranchPattern => { if args .branch .iter() .any(|pattern| pattern.as_exact().map_or(false, |s| s.contains('*'))) { user_error_with_hint( err.to_string(), "Prefix the pattern with `glob:` to expand `*` as a glob", ) } else { user_error(err.to_string()) } } GitFetchError::GitImportError(err) => err.into(), GitFetchError::InternalGitError(err) => map_git_error(err), _ => user_error(err.to_string()), })?; print_git_import_stats(ui, &stats.import_stats)?; } tx.finish( ui, format!("fetch from git remote(s) {}", remotes.iter().join(",")), )?; Ok(())} fn get_single_remote(git_repo: &git2::Repository) -> Result<Option<String>, CommandError> { let git_remotes = git_repo.remotes()?; Ok(match git_remotes.len() { 1 => git_remotes.get(0).map(ToOwned::to_owned), _ => None, })} const DEFAULT_REMOTE: &str = "origin"; fn get_default_fetch_remotes( ui: &Ui, settings: &UserSettings, git_repo: &git2::Repository,) -> Result<Vec<String>, CommandError> { const KEY: &str = "git.fetch"; if let Ok(remotes) = settings.config().get(KEY) { Ok(remotes) } else if let Some(remote) = settings.config().get_string(KEY).optional()? { Ok(vec![remote]) } else if let Some(remote) = get_single_remote(git_repo)? { // if nothing was explicitly configured, try to guess if remote != DEFAULT_REMOTE { writeln!( ui.hint(), "Fetching from the only existing remote: {}", remote )?; } Ok(vec![remote]) } else { Ok(vec![DEFAULT_REMOTE.to_owned()]) }} fn get_all_remotes(git_repo: &git2::Repository) -> Result<Vec<String>, CommandError> { let git_remotes = git_repo.remotes()?; Ok(git_remotes .iter() .filter_map(|x| x.map(ToOwned::to_owned)) .collect())} fn absolute_git_source(cwd: &Path, source: &str) -> String { // Git appears to turn URL-like source to absolute path if local git directory // exits, and fails because '$PWD/https' is unsupported protocol. Since it would // be tedious to copy the exact git (or libgit2) behavior, we simply assume a // source containing ':' is a URL, SSH remote, or absolute path with Windows // drive letter. if !source.contains(':') && Path::new(source).exists() { // It's less likely that cwd isn't utf-8, so just fall back to original source. cwd.join(source) .into_os_string() .into_string() .unwrap_or_else(|_| source.to_owned()) } else { source.to_owned() }} fn clone_destination_for_source(source: &str) -> Option<&str> { let destination = source.strip_suffix(".git").unwrap_or(source); let destination = destination.strip_suffix('/').unwrap_or(destination); destination .rsplit_once(&['/', '\\', ':'][..]) .map(|(_, name)| name)} fn is_empty_dir(path: &Path) -> bool { if let Ok(mut entries) = path.read_dir() { entries.next().is_none() } else { false }} fn cmd_git_clone( ui: &mut Ui, command: &CommandHelper, args: &GitCloneArgs,) -> Result<(), CommandError> { let remote_name = "origin"; let source = absolute_git_source(command.cwd(), &args.source); let wc_path_str = args .destination .as_deref() .or_else(|| clone_destination_for_source(&source)) .ok_or_else(|| user_error("No destination specified and wasn't able to guess it"))?; let wc_path = command.cwd().join(wc_path_str); let wc_path_existed = match fs::create_dir(&wc_path) { Ok(()) => false, Err(err) if err.kind() == io::ErrorKind::AlreadyExists => true, Err(err) => return Err(user_error(format!("Failed to create {wc_path_str}: {err}"))), }; if wc_path_existed && !is_empty_dir(&wc_path) { return Err(user_error( "Destination path exists and is not an empty directory", )); } // Canonicalize because fs::remove_dir_all() doesn't seem to like e.g. // `/some/path/.` let canonical_wc_path: PathBuf = wc_path .canonicalize() .map_err(|err| user_error(format!("Failed to create {wc_path_str}: {err}")))?; let clone_result = do_git_clone( ui, command, args.colocate, remote_name, &source, &canonical_wc_path, ); if clone_result.is_err() { let clean_up_dirs = || -> io::Result<()> { fs::remove_dir_all(canonical_wc_path.join(".jj"))?; if args.colocate { fs::remove_dir_all(canonical_wc_path.join(".git"))?; } if !wc_path_existed { fs::remove_dir(&canonical_wc_path)?; } Ok(()) }; if let Err(err) = clean_up_dirs() { writeln!( ui.warning(), "Failed to clean up {}: {}", canonical_wc_path.display(), err ) .ok();Lines 1–500 of 1062. Load more
// 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 std::collections::HashSet; use std::io::Write; use std::ops::Deref; use std::path::{Path, PathBuf}; use std::{fmt, fs, io}; use clap::{ArgGroup, Subcommand}; use itertools::Itertools; use jj_lib::backend::TreeValue; use jj_lib::git::{ self, parse_gitmodules, GitBranchPushTargets, GitFetchError, GitFetchStats, GitPushError, }; use jj_lib::object_id::ObjectId; use jj_lib::op_store::RefTarget; use jj_lib::refs::{ classify_branch_push_action, BranchPushAction, BranchPushUpdate, TrackingRefPair, }; use jj_lib::repo::Repo; use jj_lib::repo_path::RepoPath; use jj_lib::revset::{self, RevsetExpression, RevsetIteratorExt as _}; use jj_lib::settings::{ConfigResultExt as _, UserSettings}; use jj_lib::str_util::StringPattern; use jj_lib::view::View; use jj_lib::workspace::Workspace; use maplit::hashset; use crate::cli_util::{ parse_string_pattern, resolve_multiple_nonempty_revsets, short_change_hash, short_commit_hash, user_error, user_error_with_hint, CommandError, CommandHelper, RevisionArg, WorkspaceCommandHelper, }; use crate::git_util::{ get_git_repo, print_failed_git_export, print_git_import_stats, with_remote_git_callbacks, }; use crate::ui::Ui; /// Commands for working with the underlying Git repo /// /// For a comparison with Git, including a table of commands, see /// https://github.com/martinvonz/jj/blob/main/docs/git-comparison.md. #[derive(Subcommand, Clone, Debug)] pub enum GitCommand { #[command(subcommand)] Remote(GitRemoteCommand), Fetch(GitFetchArgs), Clone(GitCloneArgs), Push(GitPushArgs), Import(GitImportArgs), Export(GitExportArgs), #[command(subcommand, hide = true)] Submodule(GitSubmoduleCommand), } /// Manage Git remotes /// /// The Git repo will be a bare git repo stored inside the `.jj/` directory. #[derive(Subcommand, Clone, Debug)] pub enum GitRemoteCommand { Add(GitRemoteAddArgs), Remove(GitRemoteRemoveArgs), Rename(GitRemoteRenameArgs), List(GitRemoteListArgs), } /// Add a Git remote #[derive(clap::Args, Clone, Debug)] pub struct GitRemoteAddArgs { /// The remote's name remote: String, /// The remote's URL url: String, } /// Remove a Git remote and forget its branches #[derive(clap::Args, Clone, Debug)] pub struct GitRemoteRemoveArgs { /// The remote's name remote: String, } /// Rename a Git remote #[derive(clap::Args, Clone, Debug)] pub struct GitRemoteRenameArgs { /// The name of an existing remote old: String, /// The desired name for `old` new: String, } /// List Git remotes #[derive(clap::Args, Clone, Debug)] pub struct GitRemoteListArgs {} /// Fetch from a Git remote #[derive(clap::Args, Clone, Debug)] pub struct GitFetchArgs { /// Fetch only some of the branches /// /// By default, the specified name matches exactly. Use `glob:` prefix to /// expand `*` as a glob. The other wildcard characters aren't supported. #[arg(long, default_value = "glob:*", value_parser = parse_string_pattern)] branch: Vec<StringPattern>, /// The remote to fetch from (only named remotes are supported, can be /// repeated) #[arg(long = "remote", value_name = "remote")] remotes: Vec<String>, /// Fetch from all remotes #[arg(long, conflicts_with = "remotes")] all_remotes: bool, } /// Create a new repo backed by a clone of a Git repo /// /// The Git repo will be a bare git repo stored inside the `.jj/` directory. #[derive(clap::Args, Clone, Debug)] pub struct GitCloneArgs { /// URL or path of the Git repo to clone #[arg(value_hint = clap::ValueHint::DirPath)] source: String, /// The directory to write the Jujutsu repo to #[arg(value_hint = clap::ValueHint::DirPath)] destination: Option<String>, /// Whether or not to colocate the Jujutsu repo with the git repo #[arg(long)] colocate: bool, } /// Push to a Git remote /// /// By default, pushes any branches pointing to /// `remote_branches(remote=<remote>)..@`. Use `--branch` to push specific /// branches. Use `--all` to push all branches. Use `--change` to generate /// branch names based on the change IDs of specific commits. #[derive(clap::Args, Clone, Debug)] #[command(group(ArgGroup::new("specific").args(&["branch", "change", "revisions"]).multiple(true)))] #[command(group(ArgGroup::new("what").args(&["all", "deleted"]).conflicts_with("specific")))] pub struct GitPushArgs { /// The remote to push to (only named remotes are supported) #[arg(long)] remote: Option<String>, /// Push only this branch (can be repeated) /// /// By default, the specified name matches exactly. Use `glob:` prefix to /// select branches by wildcard pattern. For details, see /// https://github.com/martinvonz/jj/blob/main/docs/revsets.md#string-patterns. #[arg(long, short, value_parser = parse_string_pattern)] branch: Vec<StringPattern>, /// Push all branches (including deleted branches) #[arg(long)] all: bool, /// Push all deleted branches #[arg(long)] deleted: bool, /// Push branches pointing to these commits #[arg(long, short)] revisions: Vec<RevisionArg>, /// Push this commit by creating a branch based on its change ID (can be /// repeated) #[arg(long, short)] change: Vec<RevisionArg>, /// Only display what will change on the remote #[arg(long)] dry_run: bool, } /// Update repo with changes made in the underlying Git repo #[derive(clap::Args, Clone, Debug)] pub struct GitImportArgs {} /// Update the underlying Git repo with changes made in the repo #[derive(clap::Args, Clone, Debug)] pub struct GitExportArgs {} /// FOR INTERNAL USE ONLY Interact with git submodules #[derive(Subcommand, Clone, Debug)] pub enum GitSubmoduleCommand { /// Print the relevant contents from .gitmodules. For debugging purposes /// only. PrintGitmodules(GitSubmodulePrintGitmodulesArgs), } /// Print debugging info about Git submodules #[derive(clap::Args, Clone, Debug)] #[command(hide = true)] pub struct GitSubmodulePrintGitmodulesArgs { /// Read .gitmodules from the given revision. #[arg(long, short = 'r', default_value = "@")] revisions: RevisionArg, } fn make_branch_term(branch_names: &[impl fmt::Display]) -> String { match branch_names { [branch_name] => format!("branch {}", branch_name), branch_names => format!("branches {}", branch_names.iter().join(", ")), } } fn map_git_error(err: git2::Error) -> CommandError { if err.class() == git2::ErrorClass::Ssh { let hint = if err.code() == git2::ErrorCode::Certificate && std::env::var_os("HOME").is_none() { "The HOME environment variable is not set, and might be required for Git to \ successfully load certificates. Try setting it to the path of a directory that \ contains a `.ssh` directory." } else { "Jujutsu uses libssh2, which doesn't respect ~/.ssh/config. Does `ssh -F \ /dev/null` to the host work?" }; user_error_with_hint(err.to_string(), hint) } else { user_error(err.to_string()) } } pub fn maybe_add_gitignore(workspace_command: &WorkspaceCommandHelper) -> Result<(), CommandError> { if workspace_command.working_copy_shared_with_git() { std::fs::write( workspace_command .workspace_root() .join(".jj") .join(".gitignore"), "/*\n", ) .map_err(|e| user_error(format!("Failed to write .jj/.gitignore file: {e}"))) } else { Ok(()) } } fn cmd_git_remote_add( ui: &mut Ui, command: &CommandHelper, args: &GitRemoteAddArgs, ) -> Result<(), CommandError> { let workspace_command = command.workspace_helper(ui)?; let repo = workspace_command.repo(); let git_repo = get_git_repo(repo.store())?; git::add_remote(&git_repo, &args.remote, &args.url)?; Ok(()) } fn cmd_git_remote_remove( ui: &mut Ui, command: &CommandHelper, args: &GitRemoteRemoveArgs, ) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; let repo = workspace_command.repo(); let git_repo = get_git_repo(repo.store())?; let mut tx = workspace_command.start_transaction(); git::remove_remote(tx.mut_repo(), &git_repo, &args.remote)?; if tx.mut_repo().has_changes() { tx.finish(ui, format!("remove git remote {}", &args.remote)) } else { Ok(()) // Do not print "Nothing changed." } } fn cmd_git_remote_rename( ui: &mut Ui, command: &CommandHelper, args: &GitRemoteRenameArgs, ) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; let repo = workspace_command.repo(); let git_repo = get_git_repo(repo.store())?; let mut tx = workspace_command.start_transaction(); git::rename_remote(tx.mut_repo(), &git_repo, &args.old, &args.new)?; if tx.mut_repo().has_changes() { tx.finish( ui, format!("rename git remote {} to {}", &args.old, &args.new), ) } else { Ok(()) // Do not print "Nothing changed." } } fn cmd_git_remote_list( ui: &mut Ui, command: &CommandHelper, _args: &GitRemoteListArgs, ) -> Result<(), CommandError> { let workspace_command = command.workspace_helper(ui)?; let repo = workspace_command.repo(); let git_repo = get_git_repo(repo.store())?; for remote_name in git_repo.remotes()?.iter().flatten() { let remote = git_repo.find_remote(remote_name)?; writeln!( ui.stdout(), "{} {}", remote_name, remote.url().unwrap_or("<no URL>") )?; } Ok(()) } #[tracing::instrument(skip(ui, command))] fn cmd_git_fetch( ui: &mut Ui, command: &CommandHelper, args: &GitFetchArgs, ) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; let git_repo = get_git_repo(workspace_command.repo().store())?; let remotes = if args.all_remotes { get_all_remotes(&git_repo)? } else if args.remotes.is_empty() { get_default_fetch_remotes(ui, command.settings(), &git_repo)? } else { args.remotes.clone() }; let mut tx = workspace_command.start_transaction(); for remote in &remotes { let stats = with_remote_git_callbacks(ui, |cb| { git::fetch( tx.mut_repo(), &git_repo, remote, &args.branch, cb, &command.settings().git_settings(), ) }) .map_err(|err| match err { GitFetchError::InvalidBranchPattern => { if args .branch .iter() .any(|pattern| pattern.as_exact().map_or(false, |s| s.contains('*'))) { user_error_with_hint( err.to_string(), "Prefix the pattern with `glob:` to expand `*` as a glob", ) } else { user_error(err.to_string()) } } GitFetchError::GitImportError(err) => err.into(), GitFetchError::InternalGitError(err) => map_git_error(err), _ => user_error(err.to_string()), })?; print_git_import_stats(ui, &stats.import_stats)?; } tx.finish( ui, format!("fetch from git remote(s) {}", remotes.iter().join(",")), )?; Ok(()) } fn get_single_remote(git_repo: &git2::Repository) -> Result<Option<String>, CommandError> { let git_remotes = git_repo.remotes()?; Ok(match git_remotes.len() { 1 => git_remotes.get(0).map(ToOwned::to_owned), _ => None, }) } const DEFAULT_REMOTE: &str = "origin"; fn get_default_fetch_remotes( ui: &Ui, settings: &UserSettings, git_repo: &git2::Repository, ) -> Result<Vec<String>, CommandError> { const KEY: &str = "git.fetch"; if let Ok(remotes) = settings.config().get(KEY) { Ok(remotes) } else if let Some(remote) = settings.config().get_string(KEY).optional()? { Ok(vec![remote]) } else if let Some(remote) = get_single_remote(git_repo)? { // if nothing was explicitly configured, try to guess if remote != DEFAULT_REMOTE { writeln!( ui.hint(), "Fetching from the only existing remote: {}", remote )?; } Ok(vec![remote]) } else { Ok(vec![DEFAULT_REMOTE.to_owned()]) } } fn get_all_remotes(git_repo: &git2::Repository) -> Result<Vec<String>, CommandError> { let git_remotes = git_repo.remotes()?; Ok(git_remotes .iter() .filter_map(|x| x.map(ToOwned::to_owned)) .collect()) } fn absolute_git_source(cwd: &Path, source: &str) -> String { // Git appears to turn URL-like source to absolute path if local git directory // exits, and fails because '$PWD/https' is unsupported protocol. Since it would // be tedious to copy the exact git (or libgit2) behavior, we simply assume a // source containing ':' is a URL, SSH remote, or absolute path with Windows // drive letter. if !source.contains(':') && Path::new(source).exists() { // It's less likely that cwd isn't utf-8, so just fall back to original source. cwd.join(source) .into_os_string() .into_string() .unwrap_or_else(|_| source.to_owned()) } else { source.to_owned() } } fn clone_destination_for_source(source: &str) -> Option<&str> { let destination = source.strip_suffix(".git").unwrap_or(source); let destination = destination.strip_suffix('/').unwrap_or(destination); destination .rsplit_once(&['/', '\\', ':'][..]) .map(|(_, name)| name) } fn is_empty_dir(path: &Path) -> bool { if let Ok(mut entries) = path.read_dir() { entries.next().is_none() } else { false } } fn cmd_git_clone( ui: &mut Ui, command: &CommandHelper, args: &GitCloneArgs, ) -> Result<(), CommandError> { let remote_name = "origin"; let source = absolute_git_source(command.cwd(), &args.source); let wc_path_str = args .destination .as_deref() .or_else(|| clone_destination_for_source(&source)) .ok_or_else(|| user_error("No destination specified and wasn't able to guess it"))?; let wc_path = command.cwd().join(wc_path_str); let wc_path_existed = match fs::create_dir(&wc_path) { Ok(()) => false, Err(err) if err.kind() == io::ErrorKind::AlreadyExists => true, Err(err) => return Err(user_error(format!("Failed to create {wc_path_str}: {err}"))), }; if wc_path_existed && !is_empty_dir(&wc_path) { return Err(user_error( "Destination path exists and is not an empty directory", )); } // Canonicalize because fs::remove_dir_all() doesn't seem to like e.g. // `/some/path/.` let canonical_wc_path: PathBuf = wc_path .canonicalize() .map_err(|err| user_error(format!("Failed to create {wc_path_str}: {err}")))?; let clone_result = do_git_clone( ui, command, args.colocate, remote_name, &source, &canonical_wc_path, ); if clone_result.is_err() { let clean_up_dirs = || -> io::Result<()> { fs::remove_dir_all(canonical_wc_path.join(".jj"))?; if args.colocate { fs::remove_dir_all(canonical_wc_path.join(".git"))?; } if !wc_path_existed { fs::remove_dir(&canonical_wc_path)?; } Ok(()) }; if let Err(err) = clean_up_dirs() { writeln!( ui.warning(), "Failed to clean up {}: {}", canonical_wc_path.display(), err ) .ok();