forked from mirrors/jj
workspace: make working-copy type customizable
This add support for custom `jj` binaries to use custom working-copy backends. It works in the same way as with the other backends, i.e. we write a `.jj/working_copy/type` file when the working copy is initialized, and then we let that file control which implementation to use (see previous commit). I included an example of a (useless) working-copy implementation. I hope we can figure out a way to test the examples some day.
This commit is contained in:
parent
6bfd618275
commit
c3b45b6fd1
5 changed files with 311 additions and 11 deletions
244
cli/examples/custom-working-copy/main.rs
Normal file
244
cli/examples/custom-working-copy/main.rs
Normal file
|
@ -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<dyn Backend> = 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<dyn WorkingCopy>,
|
||||
}
|
||||
|
||||
impl ConflictsWorkingCopy {
|
||||
fn name() -> &'static str {
|
||||
"conflicts"
|
||||
}
|
||||
|
||||
fn init(
|
||||
store: Arc<Store>,
|
||||
working_copy_path: PathBuf,
|
||||
state_path: PathBuf,
|
||||
workspace_id: WorkspaceId,
|
||||
operation_id: OperationId,
|
||||
) -> Result<Self, WorkingCopyStateError> {
|
||||
let inner = LocalWorkingCopy::init(
|
||||
store,
|
||||
working_copy_path,
|
||||
state_path,
|
||||
operation_id,
|
||||
workspace_id,
|
||||
)?;
|
||||
Ok(ConflictsWorkingCopy {
|
||||
inner: Box::new(inner),
|
||||
})
|
||||
}
|
||||
|
||||
fn initializer() -> Box<WorkingCopyInitializer> {
|
||||
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<Store>, 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<Box<dyn LockedWorkingCopy>, 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<dyn LockedWorkingCopy>,
|
||||
}
|
||||
|
||||
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<MergedTreeId, SnapshotError> {
|
||||
options.base_ignores = options.base_ignores.chain("", "/.conflicts".as_bytes());
|
||||
self.inner.snapshot(options)
|
||||
}
|
||||
|
||||
fn check_out(&mut self, commit: &Commit) -> Result<CheckoutStats, CheckoutError> {
|
||||
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<RepoPath>,
|
||||
) -> Result<CheckoutStats, CheckoutError> {
|
||||
self.inner.set_sparse_patterns(new_sparse_patterns)
|
||||
}
|
||||
|
||||
fn finish(
|
||||
self: Box<Self>,
|
||||
operation_id: OperationId,
|
||||
) -> Result<Box<dyn WorkingCopy>, WorkingCopyStateError> {
|
||||
let inner = self.inner.finish(operation_id)?;
|
||||
Ok(Box::new(ConflictsWorkingCopy { inner }))
|
||||
}
|
||||
}
|
|
@ -2685,6 +2685,7 @@ pub struct CliRunner {
|
|||
app: Command,
|
||||
extra_configs: Option<config::Config>,
|
||||
store_factories: Option<StoreFactories>,
|
||||
working_copy_factories: Option<HashMap<String, WorkingCopyFactory>>,
|
||||
dispatch_fn: CliDispatchFn,
|
||||
process_global_args_fns: Vec<ProcessGlobalArgsFn>,
|
||||
}
|
||||
|
@ -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<String, WorkingCopyFactory>,
|
||||
) -> Self {
|
||||
self.working_copy_factories = Some(working_copy_factories);
|
||||
self
|
||||
}
|
||||
|
||||
/// Registers new subcommands in addition to the default ones.
|
||||
pub fn add_subcommand<C, F>(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,
|
||||
|
|
|
@ -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!(
|
||||
|
|
|
@ -97,6 +97,7 @@ fn init_working_copy(
|
|||
repo: &Arc<ReadonlyRepo>,
|
||||
workspace_root: &Path,
|
||||
jj_dir: &Path,
|
||||
working_copy_initializer: &WorkingCopyInitializer,
|
||||
workspace_id: WorkspaceId,
|
||||
) -> Result<(Box<dyn WorkingCopy>, Arc<ReadonlyRepo>), 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<ReadonlyRepo>), 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<ReadonlyRepo>,
|
||||
working_copy_initializer: &WorkingCopyInitializer,
|
||||
workspace_id: WorkspaceId,
|
||||
) -> Result<(Self, Arc<ReadonlyRepo>), 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<Store>, 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<String, WorkingCopyFactory> {
|
||||
let mut factories: HashMap<String, WorkingCopyFactory> = HashMap::new();
|
||||
factories.insert(
|
||||
|
@ -456,4 +487,11 @@ pub fn default_working_copy_factories() -> HashMap<String, WorkingCopyFactory> {
|
|||
factories
|
||||
}
|
||||
|
||||
pub type WorkingCopyInitializer = dyn Fn(
|
||||
Arc<Store>,
|
||||
PathBuf,
|
||||
PathBuf,
|
||||
WorkspaceId,
|
||||
OperationId,
|
||||
) -> Result<Box<dyn WorkingCopy>, WorkingCopyStateError>;
|
||||
pub type WorkingCopyFactory = Box<dyn Fn(&Arc<Store>, &Path, &Path) -> Box<dyn WorkingCopy>>;
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in a new issue