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::collections::HashSet;
16use std::fmt::Write as _;
17use std::io::Write;
18use std::path::Path;
19
20use clap::Subcommand;
21use itertools::Itertools;
22use jj_lib::repo_path::RepoPathBuf;
23use jj_lib::settings::UserSettings;
24use tracing::instrument;
25
26use crate::cli_util::{
27 edit_temp_file, print_checkout_stats, CommandHelper, WorkspaceCommandHelper,
28};
29use crate::command_error::{
30 internal_error, internal_error_with_message, user_error_with_message, CommandError,
31};
32use crate::ui::Ui;
33
34/// Manage which paths from the working-copy commit are present in the working
35/// copy
36#[derive(Subcommand, Clone, Debug)]
37pub(crate) enum SparseArgs {
38 List(SparseListArgs),
39 Set(SparseSetArgs),
40 Reset(SparseResetArgs),
41 Edit(SparseEditArgs),
42}
43
44/// List the patterns that are currently present in the working copy
45///
46/// By default, a newly cloned or initialized repo will have have a pattern
47/// matching all files from the repo root. That pattern is rendered as `.` (a
48/// single period).
49#[derive(clap::Args, Clone, Debug)]
50pub(crate) struct SparseListArgs {}
51
52/// Update the patterns that are present in the working copy
53///
54/// For example, if all you need is the `README.md` and the `lib/`
55/// directory, use `jj sparse set --clear --add README.md --add lib`.
56/// If you no longer need the `lib` directory, use `jj sparse set --remove lib`.
57#[derive(clap::Args, Clone, Debug)]
58pub(crate) struct SparseSetArgs {
59 /// Patterns to add to the working copy
60 #[arg(
61 long,
62 value_hint = clap::ValueHint::AnyPath,
63 value_parser = |s: &str| RepoPathBuf::from_relative_path(s),
64 )]
65 add: Vec<RepoPathBuf>,
66 /// Patterns to remove from the working copy
67 #[arg(
68 long,
69 conflicts_with = "clear",
70 value_hint = clap::ValueHint::AnyPath,
71 value_parser = |s: &str| RepoPathBuf::from_relative_path(s),
72 )]
73 remove: Vec<RepoPathBuf>,
74 /// Include no files in the working copy (combine with --add)
75 #[arg(long)]
76 clear: bool,
77}
78
79/// Reset the patterns to include all files in the working copy
80#[derive(clap::Args, Clone, Debug)]
81pub(crate) struct SparseResetArgs {}
82
83/// Start an editor to update the patterns that are present in the working copy
84#[derive(clap::Args, Clone, Debug)]
85pub(crate) struct SparseEditArgs {}
86
87#[instrument(skip_all)]
88pub(crate) fn cmd_sparse(
89 ui: &mut Ui,
90 command: &CommandHelper,
91 args: &SparseArgs,
92) -> Result<(), CommandError> {
93 match args {
94 SparseArgs::List(sub_args) => cmd_sparse_list(ui, command, sub_args),
95 SparseArgs::Set(sub_args) => cmd_sparse_set(ui, command, sub_args),
96 SparseArgs::Reset(sub_args) => cmd_sparse_reset(ui, command, sub_args),
97 SparseArgs::Edit(sub_args) => cmd_sparse_edit(ui, command, sub_args),
98 }
99}
100
101#[instrument(skip_all)]
102fn cmd_sparse_list(
103 ui: &mut Ui,
104 command: &CommandHelper,
105 _args: &SparseListArgs,
106) -> Result<(), CommandError> {
107 let workspace_command = command.workspace_helper(ui)?;
108 for path in workspace_command.working_copy().sparse_patterns()? {
109 writeln!(ui.stdout(), "{}", path.to_fs_path(Path::new("")).display())?;
110 }
111 Ok(())
112}
113
114#[instrument(skip_all)]
115fn cmd_sparse_set(
116 ui: &mut Ui,
117 command: &CommandHelper,
118 args: &SparseSetArgs,
119) -> Result<(), CommandError> {
120 let mut workspace_command = command.workspace_helper(ui)?;
121 update_sparse_patterns_with(ui, &mut workspace_command, |_ui, old_patterns| {
122 let mut new_patterns = HashSet::new();
123 if !args.clear {
124 new_patterns.extend(old_patterns.iter().cloned());
125 for path in &args.remove {
126 new_patterns.remove(path);
127 }
128 }
129 for path in &args.add {
130 new_patterns.insert(path.to_owned());
131 }
132 Ok(new_patterns.into_iter().sorted_unstable().collect())
133 })
134}
135
136#[instrument(skip_all)]
137fn cmd_sparse_reset(
138 ui: &mut Ui,
139 command: &CommandHelper,
140 _args: &SparseResetArgs,
141) -> Result<(), CommandError> {
142 let mut workspace_command = command.workspace_helper(ui)?;
143 update_sparse_patterns_with(ui, &mut workspace_command, |_ui, _old_patterns| {
144 Ok(vec![RepoPathBuf::root()])
145 })
146}
147
148#[instrument(skip_all)]
149fn cmd_sparse_edit(
150 ui: &mut Ui,
151 command: &CommandHelper,
152 _args: &SparseEditArgs,
153) -> Result<(), CommandError> {
154 let mut workspace_command = command.workspace_helper(ui)?;
155 let repo_path = workspace_command.repo().repo_path().to_owned();
156 update_sparse_patterns_with(ui, &mut workspace_command, |_ui, old_patterns| {
157 let mut new_patterns = edit_sparse(&repo_path, old_patterns, command.settings())?;
158 new_patterns.sort_unstable();
159 new_patterns.dedup();
160 Ok(new_patterns)
161 })
162}
163
164fn edit_sparse(
165 repo_path: &Path,
166 sparse: &[RepoPathBuf],
167 settings: &UserSettings,
168) -> Result<Vec<RepoPathBuf>, CommandError> {
169 let mut content = String::new();
170 for sparse_path in sparse {
171 let workspace_relative_sparse_path = sparse_path.to_fs_path(Path::new(""));
172 let path_string = workspace_relative_sparse_path.to_str().ok_or_else(|| {
173 internal_error(format!(
174 "Stored sparse path is not valid utf-8: {}",
175 workspace_relative_sparse_path.display()
176 ))
177 })?;
178 writeln!(&mut content, "{}", path_string).unwrap();
179 }
180
181 let content = edit_temp_file(
182 "sparse patterns",
183 ".jjsparse",
184 repo_path,
185 &content,
186 settings,
187 )?;
188
189 content
190 .lines()
191 .filter(|line| !line.starts_with("JJ: "))
192 .map(|line| line.trim())
193 .filter(|line| !line.is_empty())
194 .map(|line| {
195 RepoPathBuf::from_relative_path(line).map_err(|err| {
196 user_error_with_message(format!("Failed to parse sparse pattern: {line}"), err)
197 })
198 })
199 .try_collect()
200}
201
202fn update_sparse_patterns_with(
203 ui: &mut Ui,
204 workspace_command: &mut WorkspaceCommandHelper,
205 f: impl FnOnce(&mut Ui, &[RepoPathBuf]) -> Result<Vec<RepoPathBuf>, CommandError>,
206) -> Result<(), CommandError> {
207 let (mut locked_ws, wc_commit) = workspace_command.start_working_copy_mutation()?;
208 let new_patterns = f(ui, locked_ws.locked_wc().sparse_patterns()?)?;
209 let stats = locked_ws
210 .locked_wc()
211 .set_sparse_patterns(new_patterns)
212 .map_err(|err| internal_error_with_message("Failed to update working copy paths", err))?;
213 let operation_id = locked_ws.locked_wc().old_operation_id().clone();
214 locked_ws.finish(operation_id)?;
215 print_checkout_stats(ui, stats, &wc_commit)?;
216 Ok(())
217}