// 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. mod clone; mod colocation; mod export; mod fetch; mod import; mod init; mod push; mod remote; mod root; use std::io::Write as _; use clap::Subcommand; use clap::ValueEnum; use jj_lib::config::ConfigFile; use jj_lib::config::ConfigLayer; use jj_lib::config::ConfigSource; use jj_lib::git; use jj_lib::git::UnexpectedGitBackendError; use jj_lib::ref_name::RemoteName; use jj_lib::ref_name::RemoteNameBuf; use jj_lib::ref_name::RemoteRefSymbol; use jj_lib::ref_name::RemoteRefSymbolBuf; use jj_lib::revset; use jj_lib::store::Store; use self::clone::GitCloneArgs; use self::clone::cmd_git_clone; use self::colocation::GitColocationCommand; use self::colocation::cmd_git_colocation; use self::export::GitExportArgs; use self::export::cmd_git_export; use self::fetch::GitFetchArgs; use self::fetch::cmd_git_fetch; use self::import::GitImportArgs; use self::import::cmd_git_import; use self::init::GitInitArgs; use self::init::cmd_git_init; use self::push::GitPushArgs; use self::push::cmd_git_push; pub use self::push::is_push_operation; use self::remote::RemoteCommand; use self::remote::cmd_git_remote; use self::root::GitRootArgs; use self::root::cmd_git_root; use crate::cli_util::CommandHelper; use crate::cli_util::WorkspaceCommandHelper; use crate::command_error::CommandError; use crate::command_error::user_error_with_message; use crate::config::ConfigEnv; use crate::config::RawConfig; use crate::config::existing_repo_config_file; use crate::ui::Ui; /// Commands for working with Git remotes and the underlying Git repo /// /// See this [comparison], including a [table of commands]. /// /// [comparison]: /// https://docs.jj-vcs.dev/latest/git-comparison/. /// /// [table of commands]: /// https://docs.jj-vcs.dev/latest/git-command-table #[derive(Subcommand, Clone, Debug)] pub enum GitCommand { Clone(GitCloneArgs), #[command(subcommand)] Colocation(GitColocationCommand), Export(GitExportArgs), Fetch(GitFetchArgs), Import(GitImportArgs), Init(GitInitArgs), Push(GitPushArgs), #[command(subcommand)] Remote(RemoteCommand), Root(GitRootArgs), } pub async fn cmd_git( ui: &mut Ui, command: &CommandHelper, subcommand: &GitCommand, ) -> Result<(), CommandError> { match subcommand { GitCommand::Clone(args) => cmd_git_clone(ui, command, args).await, GitCommand::Colocation(subcommand) => cmd_git_colocation(ui, command, subcommand).await, GitCommand::Export(args) => cmd_git_export(ui, command, args).await, GitCommand::Fetch(args) => cmd_git_fetch(ui, command, args).await, GitCommand::Import(args) => cmd_git_import(ui, command, args).await, GitCommand::Init(args) => cmd_git_init(ui, command, args).await, GitCommand::Push(args) => cmd_git_push(ui, command, args).await, GitCommand::Remote(args) => cmd_git_remote(ui, command, args).await, GitCommand::Root(args) => cmd_git_root(ui, command, args).await, } } 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_with_message("Failed to write .jj/.gitignore file", e)) } else { Ok(()) } } fn get_single_remote(store: &Store) -> Result, UnexpectedGitBackendError> { let mut names = git::get_all_remote_names(store)?; Ok(match names.len() { 1 => names.pop(), _ => None, }) } const TRUNK_CONFIG_NAME: [&str; 2] = ["revset-aliases", "trunk()"]; #[derive(Clone, Copy, Debug)] struct RepoPresets<'a> { remote: &'a RemoteName, fetch_bookmarks: Option<&'a [String]>, fetch_tags: Option<&'a [String]>, trunk: Option>, } impl RepoPresets<'_> { fn is_default(self) -> bool { let Self { remote: _, fetch_bookmarks, fetch_tags, trunk, } = self; fetch_bookmarks.is_none() && fetch_tags.is_none() && trunk.is_none() } } /// Saves trunk and default fetch settings to repo-level config file. fn write_repo_presets( ui: &Ui, config_env: &ConfigEnv, presets: RepoPresets<'_>, ) -> Result<(), CommandError> { if presets.is_default() { return Ok(()); // Don't initialize config directory } let Some(config_path) = config_env.repo_config_path(ui)? else { // We couldn't find the user's home directory, so we skip this step. return Ok(()); }; let mut file = ConfigFile::load_or_empty(ConfigSource::Repo, config_path)?; if let Some(exprs) = presets.fetch_bookmarks { file.set_value( ["remotes", presets.remote.as_str(), "fetch-bookmarks"], join_string_expressions(exprs), ) .expect("initial repo config shouldn't have invalid values"); } if let Some(exprs) = presets.fetch_tags { file.set_value( ["remotes", presets.remote.as_str(), "fetch-tags"], join_string_expressions(exprs), ) .expect("initial repo config shouldn't have invalid values"); } if let Some(symbol) = presets.trunk { file.set_value(TRUNK_CONFIG_NAME, symbol.to_string()) .expect("initial repo config shouldn't have invalid values"); writeln!( ui.status(), "Setting the revset alias `trunk()` to `{symbol}`", )?; } file.save()?; Ok(()) } /// Renames preset trunk and remote settings in repo-level config file. fn rename_remote_in_repo_config( ui: &Ui, config: &RawConfig, old_remote: &RemoteName, new_remote: &RemoteName, ) -> Result<(), CommandError> { let Some(mut file) = existing_repo_config_file(config) else { return Ok(()); }; // [remotes.] -> [remotes.] if let Some(remotes_item) = file.data_mut().as_table_mut().get_mut("remotes") && let Some(remotes_table) = remotes_item.as_table_like_mut() && let Some(old_item) = remotes_table.remove(old_remote.as_str()) { remotes_table.insert(new_remote.as_str(), old_item); } // trunk = @ -> @ if let Some(old_symbol) = get_trunk_symbol(file.layer()) && old_symbol.remote == old_remote { let new_symbol = old_symbol.name.to_remote_symbol(new_remote); file.set_value(TRUNK_CONFIG_NAME, new_symbol.to_string()) .expect("old value was string"); writeln!( ui.status(), "Updating the revset alias `trunk()` to `{new_symbol}`.", )?; } file.save()?; Ok(()) } /// Removes preset trunk and remote settings from repo-level config file. fn remove_remote_from_repo_config( ui: &Ui, config: &RawConfig, old_remote: &RemoteName, ) -> Result<(), CommandError> { let Some(mut file) = existing_repo_config_file(config) else { return Ok(()); }; // [remotes.] if let Some(remotes_item) = file.data_mut().as_table_mut().get_mut("remotes") && let Some(remotes_table) = remotes_item.as_table_like_mut() { remotes_table.remove(old_remote.as_str()); } // trunk = @ if let Some(old_symbol) = get_trunk_symbol(file.layer()) && old_symbol.remote == old_remote { file.delete_value(TRUNK_CONFIG_NAME) .expect("old value was string"); writeln!( ui.status(), "Resetting the revset alias `trunk()` to default value.", )?; } file.save()?; Ok(()) } fn get_trunk_symbol(layer: &ConfigLayer) -> Option { if let Ok(Some(trunk_item)) = layer.look_up_item(TRUNK_CONFIG_NAME) && let Some(trunk_str) = trunk_item.as_str() && let Ok(revset::ExpressionKind::RemoteSymbol(symbol)) = revset::parse_program(trunk_str).map(|node| node.kind) { Some(symbol) } else { None } } fn join_string_expressions(exprs: &[String]) -> String { match exprs { [] => "~*".to_owned(), // no matches _ => exprs.join(" | "), // no parentheses since | is the weakest operator } } #[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)] enum FetchTagsMode { /// Always fetch all tags All, /// Only fetch tags that point to objects that are already being /// transmitted. Included, /// Do not fetch any tags None, } impl FetchTagsMode { fn as_fetch_tags(&self) -> gix::remote::fetch::Tags { match self { Self::All => gix::remote::fetch::Tags::All, Self::Included => gix::remote::fetch::Tags::Included, Self::None => gix::remote::fetch::Tags::None, } } }