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

Make working copy customizable #2388

Merged
merged 4 commits into from
Oct 17, 2023
Merged
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
2 changes: 1 addition & 1 deletion cli/examples/custom-backend/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ fn run_custom_command(
CustomCommands::InitJit => {
let wc_path = command_helper.cwd();
// Initialize a workspace with the custom backend
Workspace::init_with_backend(command_helper.settings(), wc_path, |store_path| {
Workspace::init_with_backend(command_helper.settings(), wc_path, &|store_path| {
Ok(Box::new(JitBackend::init(store_path)?))
})?;
Ok(())
Expand Down
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 }))
}
}
32 changes: 27 additions & 5 deletions cli/src/cli_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use std::collections::{HashSet, VecDeque};
use std::collections::{HashMap, HashSet, VecDeque};
use std::env::{self, ArgsOs, VarError};
use std::ffi::{OsStr, OsString};
use std::fmt::Debug;
Expand Down Expand Up @@ -64,7 +64,8 @@ use jj_lib::working_copy::{
WorkingCopyStateError,
};
use jj_lib::workspace::{
LockedWorkspace, Workspace, WorkspaceInitError, WorkspaceLoadError, WorkspaceLoader,
default_working_copy_factories, LockedWorkspace, WorkingCopyFactory, Workspace,
WorkspaceInitError, WorkspaceLoadError, WorkspaceLoader,
};
use jj_lib::{dag_walk, file_util, git, revset};
use once_cell::unsync::OnceCell;
Expand Down Expand Up @@ -501,6 +502,7 @@ pub struct CommandHelper {
layered_configs: LayeredConfigs,
maybe_workspace_loader: Result<WorkspaceLoader, CommandError>,
store_factories: StoreFactories,
working_copy_factories: HashMap<String, WorkingCopyFactory>,
}

impl CommandHelper {
Expand All @@ -515,6 +517,7 @@ impl CommandHelper {
layered_configs: LayeredConfigs,
maybe_workspace_loader: Result<WorkspaceLoader, CommandError>,
store_factories: StoreFactories,
working_copy_factories: HashMap<String, WorkingCopyFactory>,
) -> Self {
// `cwd` is canonicalized for consistency with `Workspace::workspace_root()` and
// to easily compute relative paths between them.
Expand All @@ -530,6 +533,7 @@ impl CommandHelper {
layered_configs,
maybe_workspace_loader,
store_factories,
working_copy_factories,
}
}

Expand Down Expand Up @@ -599,7 +603,11 @@ impl CommandHelper {
pub fn load_workspace(&self) -> Result<Workspace, CommandError> {
let loader = self.workspace_loader()?;
loader
.load(&self.settings, &self.store_factories)
.load(
&self.settings,
&self.store_factories,
&self.working_copy_factories,
)
.map_err(|err| map_workspace_load_error(err, &self.global_args))
}

Expand Down Expand Up @@ -2026,9 +2034,8 @@ pub fn update_working_copy(
let stats = if Some(new_commit.tree_id()) != old_tree_id.as_ref() {
// TODO: CheckoutError::ConcurrentCheckout should probably just result in a
// warning for most commands (but be an error for the checkout command)
let new_tree = new_commit.tree()?;
let stats = workspace
.check_out(repo.op_id().clone(), old_tree_id.as_ref(), &new_tree)
.check_out(repo.op_id().clone(), old_tree_id.as_ref(), new_commit)
.map_err(|err| {
CommandError::InternalError(format!(
"Failed to check out commit {}: {}",
Expand Down Expand Up @@ -2678,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 @@ -2697,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 @@ -2720,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 @@ -2790,6 +2808,9 @@ impl CliRunner {
let config = layered_configs.merge();
ui.reset(&config)?;
let settings = UserSettings::from_config(config);
let working_copy_factories = self
.working_copy_factories
.unwrap_or_else(|| default_working_copy_factories());
let command_helper = CommandHelper::new(
self.app,
cwd,
Expand All @@ -2800,6 +2821,7 @@ impl CliRunner {
layered_configs,
maybe_workspace_loader,
self.store_factories.unwrap_or_default(),
working_copy_factories,
);
(self.dispatch_fn)(ui, &command_helper)
}
Expand Down
Loading