use std::io::ErrorKind;
use std::io::Write as _;
use itertools::Itertools as _;
use jj_lib::commit::Commit;
use jj_lib::file_util::IoResultExt as _;
use jj_lib::git;
use jj_lib::op_store::RefTarget;
use jj_lib::repo::Repo as _;
use crate::cli_util::CommandHelper;
use crate::command_error::CommandError;
use crate::command_error::user_error;
use crate::command_error::user_error_with_message;
use crate::commands::git::maybe_add_gitignore;
use crate::git_util::is_colocated_git_workspace;
use crate::ui::Ui;
#[derive(clap::Args, Clone, Debug)]
pub struct GitColocationStatusArgs {}
#[derive(clap::Args, Clone, Debug)]
pub struct GitColocationEnableArgs {}
#[derive(clap::Args, Clone, Debug)]
pub struct GitColocationDisableArgs {}
#[derive(clap::Subcommand, Clone, Debug)]
pub enum GitColocationCommand {
Disable(GitColocationDisableArgs),
Enable(GitColocationEnableArgs),
Status(GitColocationStatusArgs),
}
pub async fn cmd_git_colocation(
ui: &mut Ui,
command: &CommandHelper,
subcommand: &GitColocationCommand,
) -> Result<(), CommandError> {
match subcommand {
GitColocationCommand::Disable(args) => cmd_git_colocation_disable(ui, command, args).await,
GitColocationCommand::Enable(args) => cmd_git_colocation_enable(ui, command, args).await,
GitColocationCommand::Status(args) => cmd_git_colocation_status(ui, command, args).await,
}
}
fn workspace_supports_git_colocation_commands(
workspace_command: &crate::cli_util::WorkspaceCommandHelper,
) -> Result<(), CommandError> {
git::get_git_backend(workspace_command.repo().store())?;
let repo_dir = workspace_command.workspace_root().join(".jj").join("repo");
if repo_dir.is_file() {
return Err(user_error(
"This command cannot be used in a non-main Jujutsu workspace",
));
}
Ok(())
}
async fn cmd_git_colocation_status(
ui: &mut Ui,
command: &CommandHelper,
_args: &GitColocationStatusArgs,
) -> Result<(), CommandError> {
let workspace_command = command.workspace_helper(ui)?;
workspace_supports_git_colocation_commands(&workspace_command)?;
let repo = workspace_command.repo();
let is_colocated = is_colocated_git_workspace(workspace_command.workspace(), repo);
let git_head = repo.view().git_head();
if is_colocated {
writeln!(ui.stdout(), "Workspace is currently colocated with Git.")?;
} else {
writeln!(
ui.stdout(),
"Workspace is currently not colocated with Git."
)?;
}
writeln!(
ui.stdout(),
"Last imported/exported Git HEAD: {}",
git_head
.as_merge()
.iter()
.map(|maybe_id| match maybe_id {
Some(id) => id.to_string(),
None => "(none)".to_owned(),
})
.join(", ")
)?;
if is_colocated {
writeln!(
ui.hint_default(),
"To disable colocation, run: `jj git colocation disable`"
)?;
} else {
writeln!(
ui.hint_default(),
"To enable colocation, run: `jj git colocation enable`"
)?;
}
Ok(())
}
async fn cmd_git_colocation_enable(
ui: &mut Ui,
command: &CommandHelper,
_args: &GitColocationEnableArgs,
) -> Result<(), CommandError> {
let workspace_command = command.workspace_helper(ui)?;
workspace_supports_git_colocation_commands(&workspace_command)?;
if is_colocated_git_workspace(workspace_command.workspace(), workspace_command.repo()) {
writeln!(ui.status(), "Workspace is already colocated with Git.")?;
return Ok(());
}
let wc_commit_id = workspace_command
.get_wc_commit_id()
.ok_or_else(|| user_error("This command requires a working copy"))?
.clone();
let workspace_root = workspace_command.workspace_root();
let jj_repo_path = workspace_command.repo_path();
let git_store_path = jj_repo_path.join("store").join("git");
let git_target_path = jj_repo_path.join("store").join("git_target");
let dot_git_path = workspace_root.join(".git");
std::fs::rename(&git_store_path, &dot_git_path).map_err(|err| match err.kind() {
ErrorKind::AlreadyExists | ErrorKind::DirectoryNotEmpty => {
user_error("A .git directory already exists in the workspace root. Cannot colocate.")
}
_ => user_error_with_message(
"Failed to move Git repository from .jj/repo/store/git to workspace root directory.",
err,
),
})?;
let git_target_content = "../../../.git";
std::fs::write(&git_target_path, git_target_content).context(git_target_path)?;
set_git_repo_bare(&dot_git_path, false)?;
let mut workspace_command = reload_workspace_helper(ui, command, workspace_command).await?;
maybe_add_gitignore(&workspace_command)?;
let wc_commit = workspace_command
.repo()
.store()
.get_commit_async(&wc_commit_id)
.await?;
set_git_head_to_wc_parent(ui, &mut workspace_command, &wc_commit).await?;
writeln!(
ui.status(),
"Workspace successfully converted into a colocated Jujutsu/Git workspace."
)?;
Ok(())
}
async fn cmd_git_colocation_disable(
ui: &mut Ui,
command: &CommandHelper,
_args: &GitColocationDisableArgs,
) -> Result<(), CommandError> {
let workspace_command = command.workspace_helper(ui)?;
workspace_supports_git_colocation_commands(&workspace_command)?;
if !is_colocated_git_workspace(workspace_command.workspace(), workspace_command.repo()) {
writeln!(ui.status(), "Workspace is already not colocated with Git.")?;
return Ok(());
}
let workspace_root = workspace_command.workspace_root();
let dot_jj_path = workspace_root.join(".jj");
let git_store_path = workspace_command.repo_path().join("store").join("git");
let git_target_path = workspace_command
.repo_path()
.join("store")
.join("git_target");
let dot_git_path = workspace_root.join(".git");
let jj_gitignore_path = dot_jj_path.join(".gitignore");
std::fs::rename(&dot_git_path, &git_store_path).map_err(|e| {
user_error_with_message("Failed to move Git repository to .jj/repo/store/git", e)
})?;
set_git_repo_bare(&git_store_path, true)?;
let git_target_content = "git";
std::fs::write(&git_target_path, git_target_content).context(&git_target_path)?;
std::fs::remove_file(&jj_gitignore_path).ok();
let mut workspace_command = reload_workspace_helper(ui, command, workspace_command).await?;
remove_git_head(ui, &mut workspace_command).await?;
writeln!(
ui.status(),
"Workspace successfully converted into a non-colocated Jujutsu/Git workspace."
)?;
Ok(())
}
fn set_git_repo_bare(path: &std::path::Path, bare: bool) -> Result<(), CommandError> {
let bare_str = if bare { "true" } else { "false" };
let config_path = path.join("config");
let mut config_file =
gix::config::File::from_path_no_includes(config_path.clone(), gix::config::Source::Local)
.map_err(|err| user_error_with_message("Failed to open Git config file.", err))?;
config_file
.set_raw_value("core.bare", bare_str)
.map_err(|err| {
user_error_with_message(
format!("Failed to set core.bare to {bare_str} in Git config."),
err,
)
})?;
git::save_git_config(&config_file).map_err(|err| {
user_error_with_message(
format!(
"Failed to write to Git config file at {}.",
config_path.display()
),
err,
)
})?;
Ok(())
}
async fn set_git_head_to_wc_parent(
ui: &mut Ui,
workspace_command: &mut crate::cli_util::WorkspaceCommandHelper,
wc_commit: &Commit,
) -> Result<(), CommandError> {
let mut tx = workspace_command.start_transaction();
git::reset_head(tx.repo_mut(), wc_commit).await?;
if tx.repo().has_changes() {
tx.finish(ui, "set git head to working copy parent").await?;
}
Ok(())
}
async fn remove_git_head(
ui: &mut Ui,
workspace_command: &mut crate::cli_util::WorkspaceCommandHelper,
) -> Result<(), CommandError> {
let mut tx = workspace_command.start_transaction();
tx.repo_mut().set_git_head_target(RefTarget::absent());
if tx.repo().has_changes() {
tx.finish(ui, "remove git head reference").await?;
}
Ok(())
}
async fn reload_workspace_helper(
ui: &mut Ui,
command: &CommandHelper,
workspace_command: crate::cli_util::WorkspaceCommandHelper,
) -> Result<crate::cli_util::WorkspaceCommandHelper, CommandError> {
let workspace = command.load_workspace_at(
workspace_command.workspace_root(),
workspace_command.settings(),
)?;
let op = workspace
.repo_loader()
.load_operation(workspace_command.repo().op_id())
.await?;
let repo = workspace.repo_loader().load_at(&op).await?;
let workspace_command = command.for_workable_repo(ui, workspace, repo)?;
Ok(workspace_command)
}