use std::fs;
use futures::future::try_join_all;
use itertools::Itertools as _;
use jj_lib::commit::CommitIteratorExt as _;
use jj_lib::file_util;
use jj_lib::file_util::IoResultExt as _;
use jj_lib::ref_name::WorkspaceNameBuf;
use jj_lib::repo::Repo as _;
use jj_lib::rewrite::merge_commit_trees;
use jj_lib::workspace::Workspace;
use tracing::instrument;
use crate::cli_util::CommandHelper;
use crate::cli_util::RevisionArg;
use crate::command_error::CommandError;
use crate::command_error::internal_error_with_message;
use crate::command_error::user_error;
use crate::description_util::add_trailers;
use crate::description_util::join_message_paragraphs;
use crate::ui::Ui;
#[derive(clap::ValueEnum, Clone, Debug, Eq, PartialEq)]
enum SparseInheritance {
Copy,
Full,
Empty,
}
#[derive(clap::Args, Clone, Debug)]
pub struct WorkspaceAddArgs {
#[arg(value_hint = clap::ValueHint::DirPath)]
destination: String,
#[arg(long)]
name: Option<WorkspaceNameBuf>,
#[arg(long = "revision", short, value_name = "REVSETS", alias = "revisions")]
revisions: Vec<RevisionArg>,
#[arg(long = "message", short, value_name = "MESSAGE")]
message_paragraphs: Vec<String>,
#[arg(long, value_enum, default_value_t = SparseInheritance::Copy)]
sparse_patterns: SparseInheritance,
}
#[instrument(skip_all)]
pub async fn cmd_workspace_add(
ui: &mut Ui,
command: &CommandHelper,
args: &WorkspaceAddArgs,
) -> Result<(), CommandError> {
let old_workspace_command = command.workspace_helper(ui)?;
let destination_path = command.cwd().join(&args.destination);
let workspace_name = if let Some(name) = &args.name {
name.to_owned()
} else {
let file_name = destination_path.file_name().unwrap();
file_name
.to_str()
.ok_or_else(|| user_error("Destination path is not valid UTF-8"))?
.into()
};
if workspace_name.as_str().is_empty() {
return Err(user_error("New workspace name cannot be empty"));
}
let repo = old_workspace_command.repo();
if repo.view().get_wc_commit_id(&workspace_name).is_some() {
return Err(user_error(format!(
"Workspace named '{name}' already exists",
name = workspace_name.as_symbol()
)));
}
if !destination_path.exists() {
fs::create_dir(&destination_path).context(&destination_path)?;
} else if !file_util::is_empty_dir(&destination_path)? {
return Err(user_error(
"Destination path exists and is not an empty directory",
));
}
let working_copy_factory = command.get_working_copy_factory()?;
let repo_path = old_workspace_command.repo_path();
let (new_workspace, repo) = Workspace::init_workspace_with_existing_repo(
&destination_path,
repo_path,
repo,
working_copy_factory,
workspace_name.clone(),
)
.await?;
writeln!(
ui.status(),
"Created workspace in \"{}\"",
file_util::relative_path(command.cwd(), &destination_path).display()
)?;
if !args.destination.contains(std::path::is_separator) {
writeln!(
ui.warning_default(),
r#"Workspace created inside current directory. If this was unintentional, delete the "{}" directory and run `jj workspace forget {name}` to remove it."#,
args.destination,
name = workspace_name.as_symbol()
)?;
}
let mut new_workspace_command = command.for_workable_repo(ui, new_workspace, repo)?;
let sparsity = match args.sparse_patterns {
SparseInheritance::Full => None,
SparseInheritance::Empty => Some(vec![]),
SparseInheritance::Copy => {
let sparse_patterns = old_workspace_command
.working_copy()
.sparse_patterns()?
.to_vec();
Some(sparse_patterns)
}
};
if let Some(sparse_patterns) = sparsity {
let (mut locked_ws, _wc_commit) =
new_workspace_command.start_working_copy_mutation().await?;
locked_ws
.locked_wc()
.set_sparse_patterns(sparse_patterns)
.await
.map_err(|err| internal_error_with_message("Failed to set sparse patterns", err))?;
let operation_id = locked_ws.locked_wc().old_operation_id().clone();
locked_ws.finish(operation_id).await?;
}
let mut tx = new_workspace_command.start_transaction();
let parents = if args.revisions.is_empty() {
if let Some(old_wc_commit_id) = tx
.base_repo()
.view()
.get_wc_commit_id(old_workspace_command.workspace_name())
{
tx.repo()
.store()
.get_commit_async(old_wc_commit_id)
.await?
.parents()
.await?
} else {
vec![tx.repo().store().root_commit()]
}
} else {
try_join_all(
old_workspace_command
.resolve_some_revsets(ui, &args.revisions)
.await?
.iter()
.map(|id| tx.repo().store().get_commit_async(id)),
)
.await?
};
let tree = merge_commit_trees(tx.repo(), &parents).await?;
let parent_ids = parents.iter().ids().cloned().collect_vec();
let mut commit_builder = tx.repo_mut().new_commit(parent_ids, tree).detach();
let mut description = join_message_paragraphs(&args.message_paragraphs);
if !description.is_empty() {
commit_builder.set_description(description);
description = add_trailers(ui, &tx, &commit_builder).await?;
}
commit_builder.set_description(&description);
let new_wc_commit = commit_builder.write(tx.repo_mut()).await?;
tx.edit(&new_wc_commit)?;
tx.finish(
ui,
format!(
"create initial working-copy commit in workspace {name}",
name = workspace_name.as_symbol()
),
)
.await?;
Ok(())
}