// 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 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<Option<RemoteNameBuf>, 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<RemoteRefSymbol<'a>>,} 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.<old_remote>] -> [remotes.<new_remote>] 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 = <name>@<old_remote> -> <name>@<new_remote> 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.<old_remote>] 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 = <name>@<old_remote> 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<RemoteRefSymbolBuf> { 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(Debug, Clone, Copy, serde::Deserialize, clap::ValueEnum)]#[serde(rename_all = "lowercase")]#[value(rename_all = "lower")]enum ObjectHash { Sha1, Sha256,} impl From<ObjectHash> for gix::hash::Kind { fn from(value: ObjectHash) -> Self { match value { ObjectHash::Sha1 => Self::Sha1, ObjectHash::Sha256 => Self::Sha256, } }}
// 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 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<Option<RemoteNameBuf>, 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<RemoteRefSymbol<'a>>,
}
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.<old_remote>] -> [remotes.<new_remote>]
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 = <name>@<old_remote> -> <name>@<new_remote>
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.<old_remote>]
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 = <name>@<old_remote>
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<RemoteRefSymbolBuf> {
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(Debug, Clone, Copy, serde::Deserialize, clap::ValueEnum)]
#[serde(rename_all = "lowercase")]
#[value(rename_all = "lower")]
enum ObjectHash {
Sha1,
Sha256,
}
impl From<ObjectHash> for gix::hash::Kind {
fn from(value: ObjectHash) -> Self {
match value {
ObjectHash::Sha1 => Self::Sha1,
ObjectHash::Sha256 => Self::Sha256,
}
}
}