364 lines · 12.7 KB · Rust
RawDownload
// Copyright 2025 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::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::cli_util::WorkspaceCommandHelper;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; /// Show the current colocation status#[derive(clap::Args, Clone, Debug)]pub struct GitColocationStatusArgs {} /// Convert into a colocated Jujutsu/Git repository////// This moves the underlying Git repository that is found inside the .jj/// directory to the root of the Jujutsu workspace. This allows you to/// use Git commands directly in the Jujutsu workspace.#[derive(clap::Args, Clone, Debug)]pub struct GitColocationEnableArgs {} /// Convert into a non-colocated Jujutsu/Git repository////// This moves the Git repository that is at the root of the Jujutsu/// workspace into the .jj directory. Once this is done you will no longer/// be able to use Git commands directly in the Jujutsu workspace.#[derive(clap::Args, Clone, Debug)]pub struct GitColocationDisableArgs {} /// Manage Jujutsu repository colocation with Git#[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,    }} /// Check that the repository supports colocation commands/// which means that the repo is backed by Git and is a main workspacefn workspace_supports_git_colocation_commands(    workspace_command: &WorkspaceCommandHelper,) -> Result<(), CommandError> {    // Check if backend is Git (will show an error otherwise)    git::get_git_backend(workspace_command.repo().store())?;     // Ensure that this is the main workspace    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).await?;     // Make sure that the workspace supports git colocation commands    workspace_supports_git_colocation_commands(&workspace_command)?;     let is_colocated = is_colocated_git_workspace(workspace_command.workspace());    let git_head = workspace_command.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."        )?;    }     // git_head should be absent in non-colocated workspace, but print the    // actual status so we can debug problems.    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 if !workspace_command        .repo_path()        .join("store")        .join("git")        .exists()    {        writeln!(            ui.hint_default(),            "Colocation cannot be enabled because the workspace is backed by an external Git \             repository."        )?;    } 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).await?;     // Make sure that the workspace supports git colocation commands    workspace_supports_git_colocation_commands(&workspace_command)?;     // Then ensure that the workspace is not already colocated before proceeding    if is_colocated_git_workspace(workspace_command.workspace()) {        writeln!(ui.status(), "Workspace is already colocated with Git.")?;        return Ok(());    }     // And that it has a working copy (whose parent we'll use later to set the git    // HEAD)    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");     // Move the git repository from .jj/repo/store/git to .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.")        }        // An external Git repository (e.g. created by `jj git init        // --git-repo=<path>`) isn't managed by jj and cannot be moved.        // workspace_supports_git_colocation_commands() already ensured that        // the backend is Git.        ErrorKind::NotFound => {            let git_backend = git::get_git_backend(workspace_command.repo().store()).unwrap();            user_error(format!(                "Cannot colocate a workspace backed by an external Git repository at {}",                git_backend.git_repo_path().display()            ))        }        _ => user_error_with_message(            "Failed to move Git repository from .jj/repo/store/git to workspace root directory.",            err,        ),    })?;     // Update the git_target file to point to the new location of the git repo    let git_target_content = "../../../.git";    std::fs::write(&git_target_path, git_target_content).context(git_target_path)?;     // Then we must make the Git repository non-bare    set_git_repo_bare(&dot_git_path, false)?;     // Reload the workspace command helper to ensure it picks up the changes    let mut workspace_command = reload_workspace_helper(ui, command, workspace_command).await?;     // Add a .jj/.gitignore file (if needed) to ensure that the colocated Git    // repository does not track Jujutsu's repository    maybe_add_gitignore(&workspace_command)?;     // Finally, update git HEAD to point to the working-copy commit's parent    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).await?;     // Make sure that the repository supports git colocation commands    workspace_supports_git_colocation_commands(&workspace_command)?;     // Then ensure that the repo is colocated before proceeding    if !is_colocated_git_workspace(workspace_command.workspace()) {        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");     // Move the Git repository from .git into .jj/repo/store/git    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)    })?;     // Make the Git repository bare    set_git_repo_bare(&git_store_path, true)?;     // Update the git_target file to point to the internal git store    let git_target_content = "git";    std::fs::write(&git_target_path, git_target_content).context(&git_target_path)?;     // Remove the .jj/.gitignore file if it exists    std::fs::remove_file(&jj_gitignore_path).ok();     // Reload the workspace command helper to ensure it picks up the changes    let mut workspace_command = reload_workspace_helper(ui, command, workspace_command).await?;     // And finally, remove the git HEAD reference    remove_git_head(ui, &mut workspace_command).await?;     writeln!(        ui.status(),        "Workspace successfully converted into a non-colocated Jujutsu/Git workspace."    )?;     Ok(())} /// Set the Git repository at `path` to be bare or non-barefn 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(())} /// Set the git HEAD to the working copy commit's parentasync fn set_git_head_to_wc_parent(    ui: &mut Ui,    workspace_command: &mut 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(())} /// Remove the git HEAD referenceasync fn remove_git_head(    ui: &mut Ui,    workspace_command: &mut 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(())} /// Gets an up to date workspace helper to pick up changes made to the repoasync fn reload_workspace_helper(    ui: &mut Ui,    command: &CommandHelper,    workspace_command: WorkspaceCommandHelper,) -> Result<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)}