just playing with tangled
1// Copyright 2020 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::fs;
16
17use itertools::Itertools as _;
18use jj_lib::commit::CommitIteratorExt as _;
19use jj_lib::file_util;
20use jj_lib::file_util::IoResultExt as _;
21use jj_lib::ref_name::WorkspaceNameBuf;
22use jj_lib::repo::Repo as _;
23use jj_lib::rewrite::merge_commit_trees;
24use jj_lib::workspace::Workspace;
25use tracing::instrument;
26
27use crate::cli_util::CommandHelper;
28use crate::cli_util::RevisionArg;
29use crate::command_error::internal_error_with_message;
30use crate::command_error::user_error;
31use crate::command_error::CommandError;
32use crate::ui::Ui;
33
34/// How to handle sparse patterns when creating a new workspace.
35#[derive(clap::ValueEnum, Clone, Debug, Eq, PartialEq)]
36enum SparseInheritance {
37 /// Copy all sparse patterns from the current workspace.
38 Copy,
39 /// Include all files in the new workspace.
40 Full,
41 /// Clear all files from the workspace (it will be empty).
42 Empty,
43}
44
45/// Add a workspace
46///
47/// By default, the new workspace inherits the sparse patterns of the current
48/// workspace. You can override this with the `--sparse-patterns` option.
49#[derive(clap::Args, Clone, Debug)]
50pub struct WorkspaceAddArgs {
51 /// Where to create the new workspace
52 destination: String,
53 /// A name for the workspace
54 ///
55 /// To override the default, which is the basename of the destination
56 /// directory.
57 #[arg(long)]
58 name: Option<WorkspaceNameBuf>,
59 /// A list of parent revisions for the working-copy commit of the newly
60 /// created workspace. You may specify nothing, or any number of parents.
61 ///
62 /// If no revisions are specified, the new workspace will be created, and
63 /// its working-copy commit will exist on top of the parent(s) of the
64 /// working-copy commit in the current workspace, i.e. they will share the
65 /// same parent(s).
66 ///
67 /// If any revisions are specified, the new workspace will be created, and
68 /// the new working-copy commit will be created with all these revisions as
69 /// parents, i.e. the working-copy commit will exist as if you had run `jj
70 /// new r1 r2 r3 ...`.
71 #[arg(long, short, value_name = "REVSETS")]
72 revision: Vec<RevisionArg>,
73 /// How to handle sparse patterns when creating a new workspace.
74 #[arg(long, value_enum, default_value_t = SparseInheritance::Copy)]
75 sparse_patterns: SparseInheritance,
76}
77
78#[instrument(skip_all)]
79pub fn cmd_workspace_add(
80 ui: &mut Ui,
81 command: &CommandHelper,
82 args: &WorkspaceAddArgs,
83) -> Result<(), CommandError> {
84 let old_workspace_command = command.workspace_helper(ui)?;
85 let destination_path = command.cwd().join(&args.destination);
86 if destination_path.exists() {
87 return Err(user_error("Workspace already exists"));
88 } else {
89 fs::create_dir(&destination_path).context(&destination_path)?;
90 }
91 let workspace_name = if let Some(name) = &args.name {
92 name.to_owned()
93 } else {
94 let file_name = destination_path.file_name().unwrap();
95 file_name
96 .to_str()
97 .ok_or_else(|| user_error("Destination path is not valid UTF-8"))?
98 .into()
99 };
100 let repo = old_workspace_command.repo();
101 if repo.view().get_wc_commit_id(&workspace_name).is_some() {
102 return Err(user_error(format!(
103 "Workspace named '{name}' already exists",
104 name = workspace_name.as_symbol()
105 )));
106 }
107
108 let working_copy_factory = command.get_working_copy_factory()?;
109 let repo_path = old_workspace_command.repo_path();
110 // If we add per-workspace configuration, we'll need to reload settings for
111 // the new workspace.
112 let (new_workspace, repo) = Workspace::init_workspace_with_existing_repo(
113 &destination_path,
114 repo_path,
115 repo,
116 working_copy_factory,
117 workspace_name.clone(),
118 )?;
119 writeln!(
120 ui.status(),
121 "Created workspace in \"{}\"",
122 file_util::relative_path(command.cwd(), &destination_path).display()
123 )?;
124 // Show a warning if the user passed a path without a separator, since they
125 // may have intended the argument to only be the name for the workspace.
126 if !args.destination.contains(std::path::is_separator) {
127 writeln!(
128 ui.warning_default(),
129 r#"Workspace created inside current directory. If this was unintentional, delete the "{}" directory and run `jj workspace forget {name}` to remove it."#,
130 args.destination,
131 name = workspace_name.as_symbol()
132 )?;
133 }
134
135 let mut new_workspace_command = command.for_workable_repo(ui, new_workspace, repo)?;
136
137 let sparsity = match args.sparse_patterns {
138 SparseInheritance::Full => None,
139 SparseInheritance::Empty => Some(vec![]),
140 SparseInheritance::Copy => {
141 let sparse_patterns = old_workspace_command
142 .working_copy()
143 .sparse_patterns()?
144 .to_vec();
145 Some(sparse_patterns)
146 }
147 };
148
149 if let Some(sparse_patterns) = sparsity {
150 let checkout_options = new_workspace_command.checkout_options();
151 let (mut locked_ws, _wc_commit) = new_workspace_command.start_working_copy_mutation()?;
152 locked_ws
153 .locked_wc()
154 .set_sparse_patterns(sparse_patterns, &checkout_options)
155 .map_err(|err| internal_error_with_message("Failed to set sparse patterns", err))?;
156 let operation_id = locked_ws.locked_wc().old_operation_id().clone();
157 locked_ws.finish(operation_id)?;
158 }
159
160 let mut tx = new_workspace_command.start_transaction();
161
162 // If no parent revisions are specified, create a working-copy commit based
163 // on the parent of the current working-copy commit.
164 let parents = if args.revision.is_empty() {
165 // Check out parents of the current workspace's working-copy commit, or the
166 // root if there is no working-copy commit in the current workspace.
167 if let Some(old_wc_commit_id) = tx
168 .base_repo()
169 .view()
170 .get_wc_commit_id(old_workspace_command.workspace_name())
171 {
172 tx.repo()
173 .store()
174 .get_commit(old_wc_commit_id)?
175 .parents()
176 .try_collect()?
177 } else {
178 vec![tx.repo().store().root_commit()]
179 }
180 } else {
181 old_workspace_command
182 .resolve_some_revsets_default_single(ui, &args.revision)?
183 .iter()
184 .map(|id| tx.repo().store().get_commit(id))
185 .try_collect()?
186 };
187
188 let tree = merge_commit_trees(tx.repo(), &parents)?;
189 let parent_ids = parents.iter().ids().cloned().collect_vec();
190 let new_wc_commit = tx.repo_mut().new_commit(parent_ids, tree.id()).write()?;
191
192 tx.edit(&new_wc_commit)?;
193 tx.finish(
194 ui,
195 format!(
196 "create initial working-copy commit in workspace {name}",
197 name = workspace_name.as_symbol()
198 ),
199 )?;
200 Ok(())
201}