diff --git a/cli/examples/custom-working-copy/main.rs b/cli/examples/custom-working-copy/main.rs new file mode 100644 index 0000000000..0b32a918e7 --- /dev/null +++ b/cli/examples/custom-working-copy/main.rs @@ -0,0 +1,244 @@ +// Copyright 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. + +use std::any::Any; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use itertools::Itertools; +use jj_cli::cli_util::{CliRunner, CommandError, CommandHelper}; +use jj_cli::ui::Ui; +use jj_lib::backend::{Backend, MergedTreeId}; +use jj_lib::commit::Commit; +use jj_lib::git_backend::GitBackend; +use jj_lib::local_working_copy::LocalWorkingCopy; +use jj_lib::merged_tree::MergedTree; +use jj_lib::op_store::{OperationId, WorkspaceId}; +use jj_lib::repo::ReadonlyRepo; +use jj_lib::repo_path::RepoPath; +use jj_lib::store::Store; +use jj_lib::working_copy::{ + CheckoutError, CheckoutStats, LockedWorkingCopy, ResetError, SnapshotError, SnapshotOptions, + WorkingCopy, WorkingCopyStateError, +}; +use jj_lib::workspace::{default_working_copy_factories, WorkingCopyInitializer, Workspace}; + +#[derive(clap::Parser, Clone, Debug)] +enum CustomCommands { + /// Initialize a workspace using the "conflicts" working copy + InitConflicts, +} + +fn run_custom_command( + _ui: &mut Ui, + command_helper: &CommandHelper, + command: CustomCommands, +) -> Result<(), CommandError> { + match command { + CustomCommands::InitConflicts => { + let wc_path = command_helper.cwd(); + let backend_initializer = |store_path: &Path| { + let backend: Box = Box::new(GitBackend::init_internal(store_path)?); + Ok(backend) + }; + Workspace::init_with_factories( + command_helper.settings(), + wc_path, + &backend_initializer, + &ReadonlyRepo::default_op_store_initializer(), + &ReadonlyRepo::default_op_heads_store_initializer(), + &ReadonlyRepo::default_index_store_initializer(), + &ReadonlyRepo::default_submodule_store_initializer(), + &ConflictsWorkingCopy::initializer(), + WorkspaceId::default(), + )?; + Ok(()) + } + } +} + +fn main() -> std::process::ExitCode { + let mut working_copy_factories = default_working_copy_factories(); + working_copy_factories.insert( + ConflictsWorkingCopy::name().to_owned(), + Box::new(|store, working_copy_path, state_path| { + Box::new(ConflictsWorkingCopy::load( + store.clone(), + working_copy_path.to_owned(), + state_path.to_owned(), + )) + }), + ); + CliRunner::init() + .set_working_copy_factories(working_copy_factories) + .add_subcommand(run_custom_command) + .run() +} + +/// A working copy that adds a .conflicts file with a list of unresolved +/// conflicts. +/// +/// Most functions below just delegate to the inner working-copy backend. The +/// only interesting functions are `snapshot()` and `check_out()`. The former +/// adds `.conflicts` to the .gitignores. The latter writes the `.conflicts` +/// file to the working copy. +struct ConflictsWorkingCopy { + inner: Box, +} + +impl ConflictsWorkingCopy { + fn name() -> &'static str { + "conflicts" + } + + fn init( + store: Arc, + working_copy_path: PathBuf, + state_path: PathBuf, + workspace_id: WorkspaceId, + operation_id: OperationId, + ) -> Result { + let inner = LocalWorkingCopy::init( + store, + working_copy_path, + state_path, + operation_id, + workspace_id, + )?; + Ok(ConflictsWorkingCopy { + inner: Box::new(inner), + }) + } + + fn initializer() -> Box { + Box::new( + |store, working_copy_path, state_path, workspace_id, operation_id| { + let wc = Self::init( + store, + working_copy_path, + state_path, + workspace_id, + operation_id, + )?; + Ok(Box::new(wc)) + }, + ) + } + + fn load(store: Arc, working_copy_path: PathBuf, state_path: PathBuf) -> Self { + let inner = LocalWorkingCopy::load(store, working_copy_path, state_path); + ConflictsWorkingCopy { + inner: Box::new(inner), + } + } +} + +impl WorkingCopy for ConflictsWorkingCopy { + fn as_any(&self) -> &dyn Any { + self + } + + fn name(&self) -> &str { + Self::name() + } + + fn path(&self) -> &Path { + self.inner.path() + } + + fn workspace_id(&self) -> &WorkspaceId { + self.inner.workspace_id() + } + + fn operation_id(&self) -> &OperationId { + self.inner.operation_id() + } + + fn tree_id(&self) -> Result<&MergedTreeId, WorkingCopyStateError> { + self.inner.tree_id() + } + + fn sparse_patterns(&self) -> Result<&[RepoPath], WorkingCopyStateError> { + self.inner.sparse_patterns() + } + + fn start_mutation(&self) -> Result, WorkingCopyStateError> { + let inner = self.inner.start_mutation()?; + Ok(Box::new(LockedConflictsWorkingCopy { + wc_path: self.inner.path().to_owned(), + inner, + })) + } +} + +struct LockedConflictsWorkingCopy { + wc_path: PathBuf, + inner: Box, +} + +impl LockedWorkingCopy for LockedConflictsWorkingCopy { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn old_operation_id(&self) -> &OperationId { + self.inner.old_operation_id() + } + + fn old_tree_id(&self) -> &MergedTreeId { + self.inner.old_tree_id() + } + + fn snapshot(&mut self, mut options: SnapshotOptions) -> Result { + options.base_ignores = options.base_ignores.chain("", "/.conflicts".as_bytes()); + self.inner.snapshot(options) + } + + fn check_out(&mut self, commit: &Commit) -> Result { + let conflicts = commit + .tree()? + .conflicts() + .map(|(path, _value)| format!("{}\n", path.to_internal_file_string())) + .join(""); + std::fs::write(self.wc_path.join(".conflicts"), conflicts).unwrap(); + self.inner.check_out(commit) + } + + fn reset(&mut self, new_tree: &MergedTree) -> Result<(), ResetError> { + self.inner.reset(new_tree) + } + + fn sparse_patterns(&self) -> Result<&[RepoPath], WorkingCopyStateError> { + self.inner.sparse_patterns() + } + + fn set_sparse_patterns( + &mut self, + new_sparse_patterns: Vec, + ) -> Result { + self.inner.set_sparse_patterns(new_sparse_patterns) + } + + fn finish( + self: Box, + operation_id: OperationId, + ) -> Result, WorkingCopyStateError> { + let inner = self.inner.finish(operation_id)?; + Ok(Box::new(ConflictsWorkingCopy { inner })) + } +} diff --git a/cli/src/cli_util.rs b/cli/src/cli_util.rs index 60917483f6..3a9e7433ba 100644 --- a/cli/src/cli_util.rs +++ b/cli/src/cli_util.rs @@ -2685,6 +2685,7 @@ pub struct CliRunner { app: Command, extra_configs: Option, store_factories: Option, + working_copy_factories: Option>, dispatch_fn: CliDispatchFn, process_global_args_fns: Vec, } @@ -2704,6 +2705,7 @@ impl CliRunner { app: crate::commands::default_app(), extra_configs: None, store_factories: None, + working_copy_factories: None, dispatch_fn: Box::new(crate::commands::run_command), process_global_args_fns: vec![], } @@ -2727,6 +2729,15 @@ impl CliRunner { self } + /// Replaces working copy factories to be used. + pub fn set_working_copy_factories( + mut self, + working_copy_factories: HashMap, + ) -> Self { + self.working_copy_factories = Some(working_copy_factories); + self + } + /// Registers new subcommands in addition to the default ones. pub fn add_subcommand(mut self, custom_dispatch_fn: F) -> Self where @@ -2797,7 +2808,9 @@ impl CliRunner { let config = layered_configs.merge(); ui.reset(&config)?; let settings = UserSettings::from_config(config); - let working_copy_factories = default_working_copy_factories(); + let working_copy_factories = self + .working_copy_factories + .unwrap_or_else(|| default_working_copy_factories()); let command_helper = CommandHelper::new( self.app, cwd, diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs index a57498c22d..d703175da5 100644 --- a/cli/src/commands/mod.rs +++ b/cli/src/commands/mod.rs @@ -48,7 +48,7 @@ use jj_lib::revset_graph::{ use jj_lib::rewrite::{back_out_commit, merge_commit_trees, rebase_commit, DescendantRebaser}; use jj_lib::settings::UserSettings; use jj_lib::working_copy::SnapshotOptions; -use jj_lib::workspace::Workspace; +use jj_lib::workspace::{default_working_copy_initializer, Workspace}; use jj_lib::{conflicts, file_util, revset}; use maplit::{hashmap, hashset}; use tracing::instrument; @@ -3753,10 +3753,12 @@ fn cmd_workspace_add( "Workspace named '{name}' already exists" ))); } + // TODO: How do we create a workspace with a non-default working copy? let (new_workspace, repo) = Workspace::init_workspace_with_existing_repo( command.settings(), &destination_path, repo, + default_working_copy_initializer(), workspace_id, )?; writeln!( diff --git a/lib/src/workspace.rs b/lib/src/workspace.rs index ef91b47ef1..b699af9587 100644 --- a/lib/src/workspace.rs +++ b/lib/src/workspace.rs @@ -97,6 +97,7 @@ fn init_working_copy( repo: &Arc, workspace_root: &Path, jj_dir: &Path, + working_copy_initializer: &WorkingCopyInitializer, workspace_id: WorkspaceId, ) -> Result<(Box, Arc), WorkspaceInitError> { let working_copy_state_path = jj_dir.join("working_copy"); @@ -113,14 +114,16 @@ fn init_working_copy( )?; let repo = tx.commit(); - let working_copy = LocalWorkingCopy::init( + let working_copy = working_copy_initializer( repo.store().clone(), workspace_root.to_path_buf(), - working_copy_state_path, - repo.op_id().clone(), + working_copy_state_path.clone(), workspace_id, + repo.op_id().clone(), )?; - Ok((Box::new(working_copy), repo)) + let working_copy_type_path = working_copy_state_path.join("type"); + fs::write(&working_copy_type_path, working_copy.name()).context(&working_copy_type_path)?; + Ok((working_copy, repo)) } impl Workspace { @@ -195,6 +198,7 @@ impl Workspace { op_heads_store_initializer: &OpHeadsStoreInitializer, index_store_initializer: &IndexStoreInitializer, submodule_store_initializer: &SubmoduleStoreInitializer, + working_copy_initializer: &WorkingCopyInitializer, workspace_id: WorkspaceId, ) -> Result<(Self, Arc), WorkspaceInitError> { let jj_dir = create_jj_dir(workspace_root)?; @@ -214,8 +218,14 @@ impl Workspace { RepoInitError::Backend(err) => WorkspaceInitError::Backend(err), RepoInitError::Path(err) => WorkspaceInitError::Path(err), })?; - let (working_copy, repo) = - init_working_copy(user_settings, &repo, workspace_root, &jj_dir, workspace_id)?; + let (working_copy, repo) = init_working_copy( + user_settings, + &repo, + workspace_root, + &jj_dir, + working_copy_initializer, + workspace_id, + )?; let repo_loader = repo.loader(); let workspace = Workspace::new(workspace_root, working_copy, repo_loader)?; Ok((workspace, repo)) @@ -239,6 +249,7 @@ impl Workspace { ReadonlyRepo::default_op_heads_store_initializer(), ReadonlyRepo::default_index_store_initializer(), ReadonlyRepo::default_submodule_store_initializer(), + default_working_copy_initializer(), WorkspaceId::default(), ) } @@ -247,6 +258,7 @@ impl Workspace { user_settings: &UserSettings, workspace_root: &Path, repo: &Arc, + working_copy_initializer: &WorkingCopyInitializer, workspace_id: WorkspaceId, ) -> Result<(Self, Arc), WorkspaceInitError> { let jj_dir = create_jj_dir(workspace_root)?; @@ -263,8 +275,14 @@ impl Workspace { ) .context(&repo_file_path)?; - let (working_copy, repo) = - init_working_copy(user_settings, repo, workspace_root, &jj_dir, workspace_id)?; + let (working_copy, repo) = init_working_copy( + user_settings, + repo, + workspace_root, + &jj_dir, + working_copy_initializer, + workspace_id, + )?; let workspace = Workspace::new(workspace_root, working_copy, repo.loader())?; Ok((workspace, repo)) } @@ -441,6 +459,19 @@ impl WorkspaceLoader { } } +pub fn default_working_copy_initializer() -> &'static WorkingCopyInitializer { + &|store: Arc, working_copy_path, state_path, workspace_id, operation_id| { + let wc = LocalWorkingCopy::init( + store, + working_copy_path, + state_path, + operation_id, + workspace_id, + )?; + Ok(Box::new(wc)) + } +} + pub fn default_working_copy_factories() -> HashMap { let mut factories: HashMap = HashMap::new(); factories.insert( @@ -456,4 +487,11 @@ pub fn default_working_copy_factories() -> HashMap { factories } +pub type WorkingCopyInitializer = dyn Fn( + Arc, + PathBuf, + PathBuf, + WorkspaceId, + OperationId, +) -> Result, WorkingCopyStateError>; pub type WorkingCopyFactory = Box, &Path, &Path) -> Box>; diff --git a/lib/tests/test_workspace.rs b/lib/tests/test_workspace.rs index db6f5944c4..cb5d8767f4 100644 --- a/lib/tests/test_workspace.rs +++ b/lib/tests/test_workspace.rs @@ -15,7 +15,9 @@ use assert_matches::assert_matches; use jj_lib::op_store::WorkspaceId; use jj_lib::repo::Repo; -use jj_lib::workspace::{default_working_copy_factories, Workspace, WorkspaceLoadError}; +use jj_lib::workspace::{ + default_working_copy_factories, default_working_copy_initializer, Workspace, WorkspaceLoadError, +}; use testutils::{TestRepo, TestWorkspace}; #[test] @@ -49,6 +51,7 @@ fn test_init_additional_workspace() { &settings, &ws2_root, &test_workspace.repo, + default_working_copy_initializer(), ws2_id.clone(), ) .unwrap();