1use std::path::Path;
5
6use crate::common::compare::{diff_content_digest, diff_xvc_path_metadata};
7use crate::common::gitignore::make_ignore_handler;
8use crate::common::{filter_targets_from_store, xvc_path_metadata_map_from_disk, FileTextOrBinary};
9use crate::error::Error;
10use crate::recheck::{make_recheck_handler, RecheckOperation};
11use crate::Result;
12use anyhow::anyhow;
13use clap::Parser;
14
15use clap_complete::ArgValueCompleter;
16use xvc_core::util::completer::{strum_variants_completer, xvc_path_completer};
17use xvc_core::{debug, error, XvcOutputSender};
18use xvc_core::{ContentDigest, Diff, RecheckMethod, XvcFileType, XvcMetadata, XvcPath, XvcRoot};
19use xvc_core::{HStore, R11Store, XvcEntity, XvcStore};
20
21#[derive(Debug, Clone, PartialEq, Eq, Parser)]
23#[command(rename_all = "kebab-case", author, version)]
24pub struct CopyCLI {
25 #[arg(long, alias = "as", add = ArgValueCompleter::new(strum_variants_completer::<RecheckMethod>) )]
29 pub recheck_method: Option<RecheckMethod>,
30
31 #[arg(long)]
33 pub force: bool,
34
35 #[arg(long)]
39 pub no_recheck: bool,
40
41 #[arg(long)]
44 pub name_only: bool,
45
46 #[arg(add = ArgValueCompleter::new(xvc_path_completer))]
53 pub source: String,
54
55 #[arg()]
66 pub destination: String,
67}
68
69pub(crate) fn get_source_path_metadata(
70 output_snd: &XvcOutputSender,
71 xvc_root: &XvcRoot,
72 stored_xvc_path_store: &XvcStore<XvcPath>,
73 stored_xvc_metadata_store: &XvcStore<XvcMetadata>,
74 source: &str,
75 destination: &str,
76) -> Result<(HStore<XvcPath>, HStore<XvcMetadata>)> {
77 let source_targets = if source.ends_with('/') {
78 let mut source = source.to_string();
79 source.push('*');
80 vec![source]
81 } else {
82 vec![source.to_string()]
83 };
84
85 let current_dir = xvc_root.config().current_dir()?;
86 let all_sources = filter_targets_from_store(
87 output_snd,
88 xvc_root,
89 stored_xvc_path_store,
90 current_dir,
91 &Some(source_targets),
92 )?;
93 let source_metadata = stored_xvc_metadata_store.subset(all_sources.keys().copied())?;
94 let source_metadata_files = source_metadata.filter(|_xe, md| md.is_file()).cloned();
95
96 if source_metadata_files.len() > 1 && !destination.ends_with('/') {
97 return Err(anyhow!("Target must be a directory if multiple sources are given").into());
98 }
99
100 let source_xvc_path_files = all_sources.subset(source_metadata_files.keys().copied())?;
101
102 Ok((source_xvc_path_files, source_metadata_files))
103}
104
105pub(crate) fn check_if_destination_is_a_directory(
106 dir_path: &XvcPath,
107 stored_xvc_path_store: &XvcStore<XvcPath>,
108 stored_metadata_store: &XvcStore<XvcMetadata>,
109) -> Result<()> {
110 let current_dir_entity = stored_xvc_path_store.entities_for(dir_path).map(|v| v[0]);
111
112 let current_dir_metadata = current_dir_entity.and_then(|e| stored_metadata_store.get(&e));
113
114 if let Some(current_dir_metadata) = current_dir_metadata {
115 if !current_dir_metadata.is_dir() {
116 return Err(anyhow!(
117 "Destination is not recorded as a directory. Please move or delete the destination first."
118 )
119 .into());
120 }
121 }
122 Ok(())
123}
124
125pub(crate) fn check_if_sources_have_changed(
126 output_snd: &XvcOutputSender,
127 xvc_root: &XvcRoot,
128 stored_xvc_path_store: &XvcStore<XvcPath>,
129 stored_metadata_store: &XvcStore<XvcMetadata>,
130 source_xvc_paths: &HStore<XvcPath>,
131 _source_metadata: &HStore<XvcMetadata>,
132) -> Result<()> {
133 let pmm = xvc_path_metadata_map_from_disk(xvc_root, source_xvc_paths);
135 let xvc_path_metadata_diff =
136 diff_xvc_path_metadata(xvc_root, stored_xvc_path_store, stored_metadata_store, &pmm);
137 let stored_content_digest_store = xvc_root.load_store::<ContentDigest>()?;
138 let stored_text_or_binary_store = xvc_root.load_store::<FileTextOrBinary>()?;
139 let content_digest_diff = diff_content_digest(
140 output_snd,
141 xvc_root,
142 stored_xvc_path_store,
143 stored_metadata_store,
144 &stored_content_digest_store,
145 &stored_text_or_binary_store,
146 &xvc_path_metadata_diff.0,
147 &xvc_path_metadata_diff.1,
148 None,
149 None,
150 true,
151 );
152 let changed_path_entities = content_digest_diff
153 .iter()
154 .filter_map(|(e, diff)| {
155 if matches!(diff, Diff::<ContentDigest>::Different { .. }) {
157 Some((*e, stored_xvc_path_store.get(e).cloned().unwrap()))
158 } else {
159 None
160 }
161 })
162 .collect::<HStore<XvcPath>>();
163
164 if !changed_path_entities.is_empty() {
165 Err(Error::SourcesHaveChanged {
166 message:
167 "Sources have changed, please carry-in or recheck following files before copying"
168 .into(),
169 files: changed_path_entities
170 .values()
171 .map(|p| p.to_string())
172 .collect::<Vec<String>>()
173 .join("\n"),
174 })
175 } else {
176 Ok(())
177 }
178}
179
180#[allow(clippy::too_many_arguments)]
185pub fn get_copy_source_dest_store(
186 output_snd: &XvcOutputSender,
187 xvc_root: &XvcRoot,
188 stored_xvc_path_store: &XvcStore<XvcPath>,
189 stored_xvc_metadata_store: &XvcStore<XvcMetadata>,
190 source_xvc_paths: &HStore<XvcPath>,
191 source_xvc_metadata: &HStore<XvcMetadata>,
192 destination: &str,
193 name_only: bool,
194 force: bool,
195) -> Result<HStore<(XvcEntity, XvcPath)>> {
196 let source_dest_store = if destination.ends_with('/') {
201 let dir_path = XvcPath::new(
202 xvc_root,
203 xvc_root,
204 Path::new(destination.strip_suffix('/').unwrap()),
205 )?;
206
207 check_if_destination_is_a_directory(
208 &dir_path,
209 stored_xvc_path_store,
210 stored_xvc_metadata_store,
211 )?;
212
213 check_if_sources_have_changed(
214 output_snd,
215 xvc_root,
216 stored_xvc_path_store,
217 stored_xvc_metadata_store,
218 source_xvc_paths,
219 source_xvc_metadata,
220 )?;
221
222 let mut source_dest_store = HStore::new();
223
224 for (source_xe, source_path) in source_xvc_paths.iter() {
225 let dest_path = if name_only {
226 dir_path.join_file_name(source_path)?
227 } else {
228 dir_path.join(source_path)?
229 };
230
231 match stored_xvc_path_store.entities_for(&dest_path) {
232 Some(v) => {
233 if !force {
234 error!(
235 output_snd,
236 "Destination file {} already exists. Use --force to overwrite.",
237 dest_path
238 );
239 continue;
240 } else {
241 source_dest_store.insert(*source_xe, (v[0], dest_path));
242 }
243 }
244 None => {
245 source_dest_store.insert(*source_xe, (xvc_root.new_entity(), dest_path));
246 }
247 }
248 }
249 source_dest_store
250 } else {
251 if source_xvc_paths.len() > 1 {
253 return Err(
254 anyhow!("Destination must be a directory if multiple sources are given").into(),
255 );
256 }
257
258 check_if_sources_have_changed(
259 output_snd,
260 xvc_root,
261 stored_xvc_path_store,
262 stored_xvc_metadata_store,
263 source_xvc_paths,
264 source_xvc_metadata,
265 )?;
266
267 let current_dir = xvc_root.config().current_dir()?;
268 let source_xe = source_xvc_paths.keys().next().unwrap();
269
270 let mut source_dest_store = HStore::<(XvcEntity, XvcPath)>::with_capacity(1);
271 let dest_path = XvcPath::new(xvc_root, current_dir, Path::new(destination))?;
272
273 match stored_xvc_path_store.entities_for(&dest_path) {
274 Some(dest_xe) => {
275 if !force {
276 return Err(anyhow!(
277 "Destination file {} already exists. Use --force to overwrite.",
278 dest_path
279 )
280 .into());
281 } else {
282 source_dest_store.insert(*source_xe, (dest_xe[0], dest_path));
283 }
284 }
285 None => {
286 source_dest_store.insert(*source_xe, (xvc_root.new_entity(), dest_path));
287 }
288 }
289 source_dest_store
290 };
291
292 Ok(source_dest_store)
293}
294
295pub(crate) fn recheck_destination(
296 output_snd: &XvcOutputSender,
297 xvc_root: &XvcRoot,
298 destination_entities: &[XvcEntity],
299) -> Result<()> {
300 let (ignore_writer, ignore_thread) = make_ignore_handler(output_snd, xvc_root)?;
301 let (recheck_handler, recheck_thread) =
302 make_recheck_handler(output_snd, xvc_root, &ignore_writer)?;
303 let stored_xvc_path_store = xvc_root.load_store::<XvcPath>()?;
306 let mut recheck_paths = stored_xvc_path_store.subset(destination_entities.iter().copied())?;
307 let stored_content_digests = xvc_root.load_store::<ContentDigest>()?;
308 let stored_recheck_methods = xvc_root.load_store::<RecheckMethod>()?;
309
310 recheck_paths.drain().for_each(|(xe, xvc_path)| {
311 let content_digest = stored_content_digests.get(&xe).unwrap();
312 let recheck_method = stored_recheck_methods.get(&xe).unwrap();
313 recheck_handler
314 .send(Some(RecheckOperation::Recheck {
315 xvc_path,
316 content_digest: *content_digest,
317 recheck_method: *recheck_method,
318 }))
319 .unwrap();
320 });
321
322 recheck_handler.send(None).unwrap();
324 recheck_thread.join().unwrap();
325 ignore_writer.send(None).unwrap();
326 ignore_thread.join().unwrap();
327
328 Ok(())
329}
330
331pub fn cmd_copy(output_snd: &XvcOutputSender, xvc_root: &XvcRoot, opts: CopyCLI) -> Result<()> {
334 let stored_metadata_store = xvc_root.load_store::<XvcMetadata>()?;
337 let stored_xvc_path_store = xvc_root.load_store::<XvcPath>()?;
338 let (source_xvc_paths, source_metadata) = get_source_path_metadata(
339 output_snd,
340 xvc_root,
341 &stored_xvc_path_store,
342 &stored_metadata_store,
343 &opts.source,
344 &opts.destination,
345 )?;
346
347 let source_dest_store = get_copy_source_dest_store(
348 output_snd,
349 xvc_root,
350 &stored_xvc_path_store,
351 &stored_metadata_store,
352 &source_xvc_paths,
353 &source_metadata,
354 &opts.destination,
355 opts.name_only,
356 opts.force,
357 )?;
358
359 xvc_root.with_r11store_mut(|store: &mut R11Store<XvcPath, XvcMetadata>| {
360 for (source_xe, (dest_xe, dest_path)) in source_dest_store.iter() {
361 let source_md = stored_metadata_store.get(source_xe).unwrap();
362 store.left.insert(*dest_xe, dest_path.clone());
363 store.right.insert(*dest_xe, *source_md);
366
367 for parent in dest_path.parents() {
369 let parent_entities = store.left.entities_for(&parent);
370 if parent_entities.is_none() || parent_entities.unwrap().is_empty() {
371 let parent_entity = xvc_root.new_entity();
372 store.left.insert(parent_entity, parent.clone());
373 store.right.insert(
374 parent_entity,
375 XvcMetadata {
376 file_type: XvcFileType::Directory,
377 ..Default::default()
378 },
379 );
380 }
381 }
382 }
383
384 xvc_root.with_store_mut(|content_digest_store: &mut XvcStore<ContentDigest>| {
387 for (source_xe, (dest_xe, _)) in source_dest_store.iter() {
388 let cd = content_digest_store.get(source_xe);
389 match cd {
390 Some(cd) => {
391 content_digest_store.insert(*dest_xe, *cd);
392 }
393 None => {
394 debug!(output_snd, "No content digest found for {}", source_xe);
395 }
396 }
397 }
398 Ok(())
399 })?;
400
401 xvc_root.with_store_mut(|text_or_binary_store: &mut XvcStore<FileTextOrBinary>| {
402 for (source_xe, (dest_xe, _)) in source_dest_store.iter() {
403 let tob = text_or_binary_store.get(source_xe);
404 match tob {
405 Some(tob) => {
406 text_or_binary_store.insert(*dest_xe, *tob);
407 }
408 None => {
409 debug!(output_snd, "No text or binary found for {}", source_xe);
410 }
411 }
412 }
413 Ok(())
414 })?;
415
416 xvc_root.with_store_mut(|recheck_method_store: &mut XvcStore<RecheckMethod>| {
417 for (source_xe, (dest_xe, _)) in source_dest_store.iter() {
418 if let Some(recheck_method) = opts.recheck_method {
419 recheck_method_store.insert(*dest_xe, recheck_method);
420 } else {
421 let source_recheck_method = recheck_method_store.get(source_xe).unwrap();
422 recheck_method_store.insert(*dest_xe, *source_recheck_method);
423 }
424 }
425 Ok(())
426 })?;
427
428 Ok(())
429 })?;
430
431 if !opts.no_recheck {
437 recheck_destination(
438 output_snd,
439 xvc_root,
440 source_dest_store
441 .iter()
442 .map(|(_, (dest_xe, _))| *dest_xe)
443 .collect::<Vec<XvcEntity>>()
444 .as_slice(),
445 )?;
446 }
447
448 Ok(())
449}