Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
config is `true`. `jj workspace forget` removes the corresponding Git
worktree when one exists.

* `jj git colocation status`/`enable`/`disable` now work on child
workspaces. `status` correctly reports colocation state and includes
the workspace name. `enable` creates a Git worktree and `disable`
removes it, allowing colocation to be toggled after workspace
creation.

### Fixed bugs

* The default pager flags now include `-K` (`--quit-on-intr`), so pressing
Expand Down
208 changes: 107 additions & 101 deletions cli/src/commands/git/colocation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use itertools::Itertools as _;
use jj_lib::commit::Commit;
use jj_lib::file_util::IoResultExt as _;
use jj_lib::git;
use jj_lib::git::GitSettings;
use jj_lib::op_store::RefTarget;
use jj_lib::repo::Repo as _;

Expand All @@ -28,7 +29,9 @@ 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::create_git_worktree;
use crate::git_util::is_colocated_git_workspace;
use crate::git_util::remove_git_worktree;
use crate::ui::Ui;

/// Show the current colocation status
Expand Down Expand Up @@ -71,44 +74,30 @@ pub async fn cmd_git_colocation(
}
}

/// Check that the repository supports colocation commands
/// which means that the repo is backed by Git and is a main workspace
fn 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)?;
git::get_git_backend(workspace_command.repo().store())?;

let is_colocated = is_colocated_git_workspace(workspace_command.workspace());
let workspace_name = workspace_command.workspace_name();
let git_head = workspace_command.repo().view().git_head(workspace_name);

if is_colocated {
writeln!(ui.stdout(), "Workspace is currently colocated with Git.")?;
writeln!(
ui.stdout(),
"Workspace '{}' is currently colocated with Git.",
workspace_name.as_symbol()
)?;
} else {
writeln!(
ui.stdout(),
"Workspace is currently not colocated with Git."
"Workspace '{}' is currently not colocated with Git.",
workspace_name.as_symbol()
)?;
}

Expand Down Expand Up @@ -159,72 +148,78 @@ async fn cmd_git_colocation_enable(
_args: &GitColocationEnableArgs,
) -> Result<(), CommandError> {
let workspace_command = command.workspace_helper(ui).await?;
let git_backend = git::get_git_backend(workspace_command.repo().store())?;

// 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!(
let is_child_workspace = workspace_command
.workspace_root()
.join(".jj")
.join("repo")
.is_file();

if is_child_workspace {
let main_workspace_root = git_backend
.git_workdir()
.ok_or_else(|| user_error("Cannot colocate: bare Git repository"))?
.to_owned();
let git_settings = GitSettings::from_settings(workspace_command.settings())?;
let workspace_root = workspace_command.workspace_root().to_owned();

create_git_worktree(ui, &git_settings, &main_workspace_root, &workspace_root)?;

let mut workspace_command = reload_workspace_helper(ui, command, workspace_command).await?;
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?;
} else {
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.",
),
ErrorKind::NotFound => 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)?;
)),
_ => user_error_with_message(
"Failed to move Git repository from .jj/repo/store/git to workspace root \
directory.",
err,
),
})?;

// Then we must make the Git repository non-bare
set_git_repo_bare(&dot_git_path, false)?;
let git_target_content = "../../../.git";
std::fs::write(&git_target_path, git_target_content).context(git_target_path)?;

// Reload the workspace command helper to ensure it picks up the changes
let mut workspace_command = reload_workspace_helper(ui, command, workspace_command).await?;
set_git_repo_bare(&dot_git_path, false)?;

// 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)?;
let mut workspace_command = reload_workspace_helper(ui, command, workspace_command).await?;
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?;
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(),
Expand All @@ -240,46 +235,57 @@ async fn cmd_git_colocation_disable(
_args: &GitColocationDisableArgs,
) -> Result<(), CommandError> {
let workspace_command = command.workspace_helper(ui).await?;
git::get_git_backend(workspace_command.repo().store())?;

// 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)?;
let is_child_workspace = workspace_command
.workspace_root()
.join(".jj")
.join("repo")
.is_file();

if is_child_workspace {
let git_backend = git::get_git_backend(workspace_command.repo().store())?;
let main_workspace_root = git_backend
.git_workdir()
.ok_or_else(|| user_error("Cannot disable colocation: bare Git repository"))?
.to_owned();
let git_settings = GitSettings::from_settings(workspace_command.settings())?;
let workspace_root = workspace_command.workspace_root().to_owned();

remove_git_worktree(ui, &git_settings, &main_workspace_root, &workspace_root)?;

let mut workspace_command = reload_workspace_helper(ui, command, workspace_command).await?;
remove_git_head(ui, &mut workspace_command).await?;
} else {
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)
})?;

// 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)?;
set_git_repo_bare(&git_store_path, true)?;

// Remove the .jj/.gitignore file if it exists
std::fs::remove_file(&jj_gitignore_path).ok();
let git_target_content = "git";
std::fs::write(&git_target_path, git_target_content).context(&git_target_path)?;

// Reload the workspace command helper to ensure it picks up the changes
let mut workspace_command = reload_workspace_helper(ui, command, workspace_command).await?;
std::fs::remove_file(&jj_gitignore_path).ok();

// And finally, remove the git HEAD reference
remove_git_head(ui, &mut workspace_command).await?;
let mut workspace_command = reload_workspace_helper(ui, command, workspace_command).await?;
remove_git_head(ui, &mut workspace_command).await?;
}

writeln!(
ui.status(),
Expand Down
10 changes: 8 additions & 2 deletions cli/src/git_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,15 @@ pub fn is_colocated_git_workspace(workspace: &Workspace) -> bool {
if git_workdir == workspace.workspace_root() {
return true;
}
// Colocated workspace should have ".git" directory, file, or symlink. Compare
let dot_git = workspace.workspace_root().join(".git");
// A .git file (gitlink) indicates a git worktree, making this a colocated
// workspace.
if dot_git.is_file() {
return true;
}
// Colocated workspace should have ".git" directory or symlink. Compare
// its parent as the git_workdir might be resolved from the real ".git" path.
let Ok(dot_git_path) = dunce::canonicalize(workspace.workspace_root().join(".git")) else {
let Ok(dot_git_path) = dunce::canonicalize(dot_git) else {
return false;
};
dunce::canonicalize(git_workdir).ok().as_deref() == dot_git_path.parent()
Expand Down
Loading
Loading