Skip to content

Commit

Permalink
workspace: make working-copy type customizable
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
martinvonz committed Oct 16, 2023
1 parent 77f861c commit 2ee7970
Show file tree
Hide file tree
Showing 5 changed files with 313 additions and 11 deletions.
244 changes: 244 additions & 0 deletions cli/examples/custom-working-copy/main.rs
Original file line number Diff line number Diff line change
@@ -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 }))
}
}
15 changes: 14 additions & 1 deletion cli/src/cli_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
}
Expand All @@ -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![],
}
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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().as_ref(),
workspace_id,
)?;
writeln!(
Expand Down
Loading

0 comments on commit 2ee7970

Please sign in to comment.