Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Colocated workspaces using git worktrees #4588

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
402abe7
workspace: Add test that colocated workspaces have independent git wo…
cormacrelf Sep 11, 2024
a29e29f
workspace: Detect git worktrees as "colocated workspaces"
cormacrelf Sep 11, 2024
69cd7a4
git: get_git_repo -> get_git_backend_repo, distinguish it from curren…
cormacrelf Sep 11, 2024
b36e80a
git: Implement git_worktree_add with nice error handling
cormacrelf Oct 4, 2024
a42bb2a
workspace: Add tests for workspace/worktree naming conflicts
cormacrelf Oct 5, 2024
4a74ae1
workspace: Implement colocated workspaces with git worktrees
cormacrelf Oct 4, 2024
0aca8be
view: deprecate git_head, introduce git_heads (a HEAD per workspace)
cormacrelf Sep 12, 2024
f0cb106
view: new operation hashes for insta due to new View.git_heads
cormacrelf Sep 12, 2024
be639d3
revset: add `git_heads()` revset, to enumerate git heads for all work…
cormacrelf Oct 5, 2024
0bad919
workspace: add failing test for `workspace forget` not removing HEAD
cormacrelf Oct 5, 2024
06480a6
view: remove git heads from view when forgetting workspace
cormacrelf Sep 13, 2024
bb2a04a
git: implement git_worktree_remove
cormacrelf Oct 5, 2024
80fd3e7
git: Remove git worktrees when forgetting workspaces
cormacrelf Oct 5, 2024
2b7b683
view: try to guess the default workspace id
cormacrelf Oct 11, 2024
3e07532
view: test for deprecated protobuf fields using the CLI
cormacrelf Oct 12, 2024
813135f
git: add test to diagnose worktree gitfiles on windows
cormacrelf Oct 13, 2024
6047297
git: fix windows gitfile paths
cormacrelf Oct 13, 2024
1d00db3
workspace: update changelog and docs for colocated workspaces
cormacrelf Oct 13, 2024
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
* New `at_operation(op, expr)` revset can be used in order to query revisions
based on historical state.

* `jj workspace add` now accepts a `--colocate` flag, which makes the new
workspace into a valid git worktree, so you can run `git` commands there
as well.

### Fixed bugs

* Error on `trunk()` revset resolution is now handled gracefully.
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ async-trait = { workspace = true }
insta = { workspace = true }
test-case = { workspace = true }
testutils = { workspace = true }
prost = { workspace = true }
# https://github.com/rust-lang/cargo/issues/2911#issuecomment-1483256987
jj-cli = { path = ".", features = ["test-fakes"], default-features = false }

Expand Down
65 changes: 54 additions & 11 deletions cli/src/cli_util.rs
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a TODO until the PR is ready but this should have a good motivation for the change. See https://github.com/martinvonz/jj/blob/main/docs/contributing.md#code-reviews particularly this section

We are not particularly strict about the style, but please do explain the reason for the change unless it's obvious.

Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,12 @@ impl CommandHelper {
let op_head = self.resolve_operation(ui, workspace.repo_loader())?;
let repo = workspace.repo_loader().load_at(&op_head)?;
let env = self.workspace_environment(ui, &workspace)?;
revset_util::warn_unresolvable_trunk(ui, repo.as_ref(), &env.revset_parse_context())?;
revset_util::warn_unresolvable_trunk(
ui,
repo.as_ref(),
&env.revset_parse_context(),
workspace.workspace_id(),
)?;
WorkspaceCommandHelper::new(ui, workspace, repo, env, self.is_at_head_operation())
}

Expand Down Expand Up @@ -645,7 +650,7 @@ impl WorkspaceCommandEnvironment {
&self.workspace_id
}

pub(crate) fn revset_parse_context(&self) -> RevsetParseContext {
pub fn revset_parse_context(&self) -> RevsetParseContext {
let workspace_context = RevsetWorkspaceContext {
path_converter: &self.path_converter,
workspace_id: &self.workspace_id,
Expand All @@ -669,7 +674,10 @@ impl WorkspaceCommandEnvironment {
/// Creates fresh new context which manages cache of short commit/change ID
/// prefixes. New context should be created per repo view (or operation.)
pub fn new_id_prefix_context(&self) -> IdPrefixContext {
let context = IdPrefixContext::new(self.command.revset_extensions().clone());
let context = IdPrefixContext::new(
self.command.revset_extensions().clone(),
Some(self.workspace_id().clone()),
);
match &self.short_prefixes_expression {
None => context,
Some(expression) => context.disambiguate_within(expression.clone()),
Expand Down Expand Up @@ -747,7 +755,10 @@ impl WorkspaceCommandEnvironment {
// Not using self.id_prefix_context() because the disambiguation data
// must not be calculated and cached against arbitrary repo. It's also
// unlikely that the immutable expression contains short hashes.
let id_prefix_context = IdPrefixContext::new(self.command.revset_extensions().clone());
let id_prefix_context = IdPrefixContext::new(
self.command.revset_extensions().clone(),
Some(self.workspace_id.clone()),
);
let to_rewrite_revset =
RevsetExpression::commits(commits.into_iter().cloned().collect_vec());
let mut expression = RevsetExpressionEvaluator::new(
Expand Down Expand Up @@ -898,7 +909,7 @@ impl WorkspaceCommandHelper {
}

/// Snapshot the working copy if allowed, and import Git refs if the working
/// copy is collocated with Git.
/// copy is colocated with Git.
#[instrument(skip_all)]
pub fn maybe_snapshot(&mut self, ui: &Ui) -> Result<(), CommandError> {
if self.may_update_working_copy {
Expand Down Expand Up @@ -928,8 +939,10 @@ impl WorkspaceCommandHelper {
fn import_git_head(&mut self, ui: &Ui) -> Result<(), CommandError> {
assert!(self.may_update_working_copy);
let command = self.env.command.clone();
let workspace_id = self.workspace_id().clone();
let git_repo = self.git_either_colocated_or_backend()?;
let mut tx = self.start_transaction();
git::import_head(tx.repo_mut())?;
git::import_head(tx.repo_mut(), &git_repo, &workspace_id)?;
if !tx.repo().has_changes() {
return Ok(());
}
Expand All @@ -945,8 +958,9 @@ impl WorkspaceCommandHelper {
// out yet.

let mut tx = tx.into_inner();
let old_git_head = self.repo().view().git_head().clone();
let new_git_head = tx.repo().view().git_head().clone();
let workspace_id = self.workspace_id();
let old_git_head = self.repo().view().git_head(workspace_id).clone();
let new_git_head = tx.repo().view().git_head(workspace_id).clone();
if let Some(new_git_head_id) = new_git_head.as_normal() {
let workspace_id = self.workspace_id().to_owned();
let new_git_head_commit = tx.repo().store().get_commit(new_git_head_id)?;
Expand Down Expand Up @@ -1074,6 +1088,36 @@ impl WorkspaceCommandHelper {
self.working_copy_shared_with_git
}

pub fn open_colocated_git_worktree_git2(
&self,
) -> Result<Option<git2::Repository>, git2::Error> {
self.working_copy_shared_with_git()
.then_some(self.workspace_root())
.map(git2::Repository::open)
.transpose()
}

pub fn open_colocated_git_worktree_gix(
&self,
) -> Result<Option<gix::Repository>, Box<gix::open::Error>> {
self.working_copy_shared_with_git()
.then_some(self.workspace_root())
.map(gix::open)
.transpose()
.map_err(Box::new)
}

pub fn git_either_colocated_or_backend(&self) -> Result<gix::Repository, CommandError> {
self.open_colocated_git_worktree_gix()
.map_err(user_error)
.transpose()
.unwrap_or_else(|| {
self.git_backend()
.ok_or_else(|| user_error("JJ repo is not backed by git"))
.map(|g| g.git_repo())
})
}

pub fn format_file_path(&self, file: &RepoPath) -> String {
self.path_converter().format_file_path(file)
}
Expand Down Expand Up @@ -1748,10 +1792,9 @@ See https://martinvonz.github.io/jj/latest/working-copy/#stale-working-copy \
.map(|commit_id| tx.repo().store().get_commit(commit_id))
.transpose()?;

if self.working_copy_shared_with_git {
let git_repo = self.git_backend().unwrap().open_git_repo()?;
if let Some(git_worktree) = self.open_colocated_git_worktree_git2()? {
if let Some(wc_commit) = &maybe_new_wc_commit {
git::reset_head(tx.repo_mut(), &git_repo, wc_commit)?;
git::reset_head(tx.repo_mut(), &git_worktree, self.workspace_id(), wc_commit)?;
}
let refs = git::export_refs(tx.repo_mut())?;
print_failed_git_export(ui, &refs)?;
Expand Down
8 changes: 6 additions & 2 deletions cli/src/commands/bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,12 @@ fn bench_revset<M: Measurement>(
let routine = |workspace_command: &WorkspaceCommandHelper, expression: Rc<RevsetExpression>| {
// Evaluate the expression without parsing/evaluating short-prefixes.
let repo = workspace_command.repo().as_ref();
let symbol_resolver =
DefaultSymbolResolver::new(repo, &([] as [Box<dyn SymbolResolverExtension>; 0]));
let workspace_id = workspace_command.workspace_id();
let symbol_resolver = DefaultSymbolResolver::new(
repo,
&([] as [Box<dyn SymbolResolverExtension>; 0]),
Some(workspace_id),
);
let resolved = expression
.resolve_user_expression(repo, &symbol_resolver)
.unwrap();
Expand Down
4 changes: 2 additions & 2 deletions cli/src/commands/git/clone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ use crate::commands::git::map_git_error;
use crate::commands::git::maybe_add_gitignore;
use crate::config::write_config_value_to_file;
use crate::config::ConfigNamePathBuf;
use crate::git_util::get_git_repo;
use crate::git_util::get_git_backend_repo;
use crate::git_util::print_git_import_stats;
use crate::git_util::with_remote_git_callbacks;
use crate::ui::Ui;
Expand Down Expand Up @@ -204,7 +204,7 @@ fn do_git_clone(
} else {
Workspace::init_internal_git(command.settings(), wc_path)?
};
let git_repo = get_git_repo(repo.store())?;
let git_repo = get_git_backend_repo(repo.store())?;
writeln!(
ui.status(),
r#"Fetching into new repo in "{}""#,
Expand Down
4 changes: 2 additions & 2 deletions cli/src/commands/git/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ use crate::command_error::user_error_with_hint;
use crate::command_error::CommandError;
use crate::commands::git::get_single_remote;
use crate::commands::git::map_git_error;
use crate::git_util::get_git_repo;
use crate::git_util::get_git_backend_repo;
use crate::git_util::print_git_import_stats;
use crate::git_util::with_remote_git_callbacks;
use crate::ui::Ui;
Expand Down Expand Up @@ -60,7 +60,7 @@ pub fn cmd_git_fetch(
args: &GitFetchArgs,
) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let git_repo = get_git_repo(workspace_command.repo().store())?;
let git_repo = get_git_backend_repo(workspace_command.repo().store())?;
let remotes = if args.all_remotes {
get_all_remotes(&git_repo)?
} else if args.remotes.is_empty() {
Expand Down
4 changes: 3 additions & 1 deletion cli/src/commands/git/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@ pub fn cmd_git_import(
_args: &GitImportArgs,
) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let workspace_id = workspace_command.workspace_id().clone();
let git_repo = workspace_command.git_either_colocated_or_backend()?;
let mut tx = workspace_command.start_transaction();
// In non-colocated repo, HEAD@git will never be moved internally by jj.
// That's why cmd_git_export() doesn't export the HEAD ref.
git::import_head(tx.repo_mut())?;
git::import_head(tx.repo_mut(), &git_repo, &workspace_id)?;
let stats = git::import_refs(tx.repo_mut(), &command.settings().git_settings())?;
print_git_import_stats(ui, tx.repo(), &stats, true)?;
tx.finish(ui, "import git refs")?;
Expand Down
16 changes: 12 additions & 4 deletions cli/src/commands/git/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ use crate::command_error::CommandError;
use crate::commands::git::maybe_add_gitignore;
use crate::config::write_config_value_to_file;
use crate::config::ConfigNamePathBuf;
use crate::git_util::get_git_repo;
use crate::git_util::get_git_backend_repo;
use crate::git_util::is_colocated_git_workspace;
use crate::git_util::print_failed_git_export;
use crate::git_util::print_git_import_stats;
Expand Down Expand Up @@ -174,9 +174,17 @@ pub fn do_init(
workspace_command.maybe_snapshot(ui)?;
maybe_set_repository_level_trunk_alias(ui, &workspace_command)?;
if !workspace_command.working_copy_shared_with_git() {
let workspace_id = workspace_command.workspace_id().clone();
let git_repo = workspace_command.git_either_colocated_or_backend()?;
let mut tx = workspace_command.start_transaction();
jj_lib::git::import_head(tx.repo_mut())?;
if let Some(git_head_id) = tx.repo().view().git_head().as_normal().cloned() {
jj_lib::git::import_head(tx.repo_mut(), &git_repo, &workspace_id)?;
if let Some(git_head_id) = tx
.repo()
.view()
.git_head(&workspace_id)
.as_normal()
.cloned()
{
let git_head_commit = tx.repo().store().get_commit(&git_head_id)?;
tx.check_out(&git_head_commit)?;
}
Expand Down Expand Up @@ -237,7 +245,7 @@ pub fn maybe_set_repository_level_trunk_alias(
ui: &Ui,
workspace_command: &WorkspaceCommandHelper,
) -> Result<(), CommandError> {
let git_repo = get_git_repo(workspace_command.repo().store())?;
let git_repo = get_git_backend_repo(workspace_command.repo().store())?;
if let Ok(reference) = git_repo.find_reference("refs/remotes/origin/HEAD") {
if let Some(reference_name) = reference.symbolic_target() {
if let Some(RefName::RemoteBranch {
Expand Down
4 changes: 2 additions & 2 deletions cli/src/commands/git/push.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ use crate::command_error::CommandError;
use crate::commands::git::get_single_remote;
use crate::commands::git::map_git_error;
use crate::formatter::Formatter;
use crate::git_util::get_git_repo;
use crate::git_util::get_git_backend_repo;
use crate::git_util::with_remote_git_callbacks;
use crate::git_util::GitSidebandProgressMessageWriter;
use crate::ui::Ui;
Expand Down Expand Up @@ -143,7 +143,7 @@ pub fn cmd_git_push(
args: &GitPushArgs,
) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let git_repo = get_git_repo(workspace_command.repo().store())?;
let git_repo = get_git_backend_repo(workspace_command.repo().store())?;

let remote = if let Some(name) = &args.remote {
name.clone()
Expand Down
4 changes: 2 additions & 2 deletions cli/src/commands/git/remote/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use jj_lib::repo::Repo;

use crate::cli_util::CommandHelper;
use crate::command_error::CommandError;
use crate::git_util::get_git_repo;
use crate::git_util::get_git_backend_repo;
use crate::ui::Ui;

/// Add a Git remote
Expand All @@ -36,7 +36,7 @@ pub fn cmd_git_remote_add(
) -> Result<(), CommandError> {
let workspace_command = command.workspace_helper(ui)?;
let repo = workspace_command.repo();
let git_repo = get_git_repo(repo.store())?;
let git_repo = get_git_backend_repo(repo.store())?;
git::add_remote(&git_repo, &args.remote, &args.url)?;
Ok(())
}
4 changes: 2 additions & 2 deletions cli/src/commands/git/remote/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use jj_lib::repo::Repo;

use crate::cli_util::CommandHelper;
use crate::command_error::CommandError;
use crate::git_util::get_git_repo;
use crate::git_util::get_git_backend_repo;
use crate::ui::Ui;

/// List Git remotes
Expand All @@ -32,7 +32,7 @@ pub fn cmd_git_remote_list(
) -> Result<(), CommandError> {
let workspace_command = command.workspace_helper(ui)?;
let repo = workspace_command.repo();
let git_repo = get_git_repo(repo.store())?;
let git_repo = get_git_backend_repo(repo.store())?;
for remote_name in git_repo.remotes()?.iter().flatten() {
let remote = git_repo.find_remote(remote_name)?;
writeln!(
Expand Down
4 changes: 2 additions & 2 deletions cli/src/commands/git/remote/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use jj_lib::repo::Repo;

use crate::cli_util::CommandHelper;
use crate::command_error::CommandError;
use crate::git_util::get_git_repo;
use crate::git_util::get_git_backend_repo;
use crate::ui::Ui;

/// Remove a Git remote and forget its bookmarks
Expand All @@ -34,7 +34,7 @@ pub fn cmd_git_remote_remove(
) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let repo = workspace_command.repo();
let git_repo = get_git_repo(repo.store())?;
let git_repo = get_git_backend_repo(repo.store())?;
let mut tx = workspace_command.start_transaction();
git::remove_remote(tx.repo_mut(), &git_repo, &args.remote)?;
if tx.repo().has_changes() {
Expand Down
4 changes: 2 additions & 2 deletions cli/src/commands/git/remote/rename.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use jj_lib::repo::Repo;

use crate::cli_util::CommandHelper;
use crate::command_error::CommandError;
use crate::git_util::get_git_repo;
use crate::git_util::get_git_backend_repo;
use crate::ui::Ui;

/// Rename a Git remote
Expand All @@ -36,7 +36,7 @@ pub fn cmd_git_remote_rename(
) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let repo = workspace_command.repo();
let git_repo = get_git_repo(repo.store())?;
let git_repo = get_git_backend_repo(repo.store())?;
let mut tx = workspace_command.start_transaction();
git::rename_remote(tx.repo_mut(), &git_repo, &args.old, &args.new)?;
if tx.repo().has_changes() {
Expand Down
4 changes: 2 additions & 2 deletions cli/src/commands/git/remote/set_url.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use jj_lib::repo::Repo;

use crate::cli_util::CommandHelper;
use crate::command_error::CommandError;
use crate::git_util::get_git_repo;
use crate::git_util::get_git_backend_repo;
use crate::ui::Ui;

/// Set the URL of a Git remote
Expand All @@ -36,7 +36,7 @@ pub fn cmd_git_remote_set_url(
) -> Result<(), CommandError> {
let workspace_command = command.workspace_helper(ui)?;
let repo = workspace_command.repo();
let git_repo = get_git_repo(repo.store())?;
let git_repo = get_git_backend_repo(repo.store())?;
git::set_remote_url(&git_repo, &args.remote, &args.url)?;
Ok(())
}
2 changes: 1 addition & 1 deletion cli/src/commands/operation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ fn view_with_desired_portions_restored(
tags: repo_source.tags.clone(),
remote_views: remote_source.remote_views.clone(),
git_refs: current_view.git_refs.clone(),
git_head: current_view.git_head.clone(),
git_heads: current_view.git_heads.clone(),
wc_commit_ids: repo_source.wc_commit_ids.clone(),
}
}
Loading