xvc_file/track/
mod.rs

1//! Home of the `xvc file track` command and related functionality.
2//!
3//! - [`cmd_track`] is the entry point for the `xvc file track` command.
4//! - [`TrackCLI`] is the command line interface
5//! - [`update_file_gitignores`] and [`update_dir_gitignores`] are functions to
6//!   update `.gitignore` files with the tracked paths.
7//! - [`carry_in`] is a specialized carry in function for `xvc file track`.
8
9use clap_complete::ArgValueCompleter;
10use derive_more::From;
11use xvc_core::util::completer::strum_variants_completer;
12
13use std::collections::HashSet;
14
15use xvc_core::util::git::build_gitignore;
16use xvc_core::UpdateFromConfig;
17use xvc_core::{FromConfig, XvcConfigResult, XvcConfiguration};
18
19use xvc_core::XvcOutputSender;
20use xvc_core::{
21    ContentDigest, HashAlgorithm, TextOrBinary, XvcCachePath, XvcFileType, XvcMetadata, XvcRoot,
22};
23
24use crate::carry_in::carry_in;
25use crate::common::compare::{
26    diff_content_digest, diff_recheck_method, diff_text_or_binary, diff_xvc_path_metadata,
27};
28use crate::common::gitignore::{update_dir_gitignores, update_file_gitignores};
29use crate::common::{targets_from_disk, update_store_records, FileTextOrBinary};
30use crate::error::Result;
31
32use clap::Parser;
33use std::path::PathBuf;
34
35use xvc_core::RecheckMethod;
36use xvc_core::XvcPath;
37use xvc_core::{HStore, XvcEntity};
38
39/// Add files for tracking with Xvc
40#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, From, Parser)]
41#[command(rename_all = "kebab-case")]
42pub struct TrackCLI {
43    /// How to track the file contents in cache: One of copy, symlink, hardlink, reflink.
44    ///
45    /// Note: Reflink uses copy if the underlying file system doesn't support it.
46    #[arg(long, alias = "as", add = ArgValueCompleter::new(strum_variants_completer::<RecheckMethod>))]
47    recheck_method: Option<RecheckMethod>,
48
49    /// Do not copy/link added files to the file cache
50    #[arg(long)]
51    no_commit: bool,
52    /// Calculate digests as text or binary file without checking contents, or by automatically. (Default:
53    /// auto)
54    #[arg(long, add = ArgValueCompleter::new(strum_variants_completer::<TextOrBinary>))]
55    text_or_binary: Option<FileTextOrBinary>,
56
57    /// Include git tracked files as well. (Default: false)
58    ///
59    /// Xvc doesn't track files that are already tracked by git by default.
60    /// You can set files.track.include-git to true in the configuration file to
61    /// change this behavior.
62    #[arg(long)]
63    include_git_files: bool,
64
65    /// Add targets even if they are already tracked
66    #[arg(long)]
67    force: bool,
68
69    /// Don't use parallelism
70    #[arg(long)]
71    no_parallel: bool,
72
73    /// Files/directories to track
74    #[arg(value_hint=clap::ValueHint::AnyPath)]
75    targets: Option<Vec<String>>,
76}
77
78impl UpdateFromConfig for TrackCLI {
79    /// Updates `xvc file` configuration from the configuration files.
80    /// Command line options take precedence over other sources.
81    /// If options are not given, they are supplied from [XvcConfig]
82    fn update_from_config(self, config: &XvcConfiguration) -> XvcConfigResult<Box<Self>> {
83        let recheck_method = self.recheck_method.unwrap_or_else(|| {
84            *RecheckMethod::from_config(config).expect("Configuration has a recheck method")
85        });
86
87        let no_commit = self.no_commit || config.file.track.no_commit;
88        let force = self.force || config.file.track.force;
89        let no_parallel = self.no_parallel || config.file.track.no_parallel;
90        let include_git_files = self.include_git_files || config.file.track.include_git_files;
91
92        let text_or_binary = Some(
93            self.text_or_binary
94                .unwrap_or_else(|| *FileTextOrBinary::from_config(config).unwrap()),
95        );
96
97        Ok(Box::new(Self {
98            targets: self.targets.clone(),
99            recheck_method: Some(recheck_method),
100            no_commit,
101            force,
102            no_parallel,
103            text_or_binary,
104            include_git_files,
105        }))
106    }
107}
108
109/// ## The pipeline
110///
111/// ```mermaid
112/// graph LR
113///     Target --> |File| Path
114///     Target -->|Directory| Dir
115///     Dir --> |File| Path
116///     Dir --> |Directory| Dir
117///     Path --> Ignored{Is this ignored?}
118///     Ignored --> |Yes| Ignore
119///     Ignored --> |No| XvcPath
120///     XvcPath --> |Force| XvcDigest
121///     XvcPath --> Filter{Is this changed?}
122///     Filter -->|Yes| XvcDigest
123///     Filter -->|No| Ignore
124///     XvcDigest --> CacheLocation
125///     CacheLocation --> RecheckMethod
126///     RecheckMethod --> |Copy| Copy
127///     RecheckMethod --> |Symlink| Symlink
128///     RecheckMethod --> |Hardlink| Hardlink
129///     RecheckMethod --> |Reflink| Reflink
130/// ```
131pub fn cmd_track(
132    output_snd: &XvcOutputSender,
133    xvc_root: &XvcRoot,
134    cli_opts: TrackCLI,
135) -> Result<()> {
136    let conf = xvc_root.config();
137    let opts = cli_opts.update_from_config(conf)?;
138    let current_dir = xvc_root.current_dir();
139    let filter_git_files = !opts.include_git_files;
140    let targets = targets_from_disk(
141        output_snd,
142        xvc_root,
143        current_dir,
144        &opts.targets,
145        filter_git_files,
146    )?;
147    let requested_recheck_method = opts.recheck_method;
148    let text_or_binary = opts.text_or_binary.unwrap_or_default();
149    let no_parallel = opts.no_parallel;
150
151    let stored_xvc_path_store = xvc_root.load_store::<XvcPath>()?;
152    let stored_xvc_metadata_store = xvc_root.load_store::<XvcMetadata>()?;
153
154    let xvc_path_metadata_diff = diff_xvc_path_metadata(
155        xvc_root,
156        &stored_xvc_path_store,
157        &stored_xvc_metadata_store,
158        &targets,
159    );
160
161    let xvc_path_diff = xvc_path_metadata_diff.0;
162    let xvc_metadata_diff = xvc_path_metadata_diff.1;
163
164    let changed_entities: HashSet<XvcEntity> =
165        xvc_path_diff
166            .iter()
167            .filter_map(|(xe, xpd)| if xpd.changed() { Some(*xe) } else { None })
168            .chain(xvc_metadata_diff.iter().filter_map(|(xe, xpd)| {
169                if xpd.changed() {
170                    Some(*xe)
171                } else {
172                    None
173                }
174            }))
175            .collect();
176
177    let stored_recheck_method_store = xvc_root.load_store::<RecheckMethod>()?;
178    let default_recheck_method = *RecheckMethod::from_config(conf)?;
179    let recheck_method_diff = diff_recheck_method(
180        default_recheck_method,
181        &stored_recheck_method_store,
182        requested_recheck_method,
183        &changed_entities,
184    );
185
186    let stored_text_or_binary_store = xvc_root.load_store::<FileTextOrBinary>()?;
187    let text_or_binary_diff = diff_text_or_binary(
188        &stored_text_or_binary_store,
189        text_or_binary,
190        &changed_entities,
191    );
192
193    let hash_algorithm = *HashAlgorithm::from_config(conf)?;
194
195    let stored_content_digest_store = xvc_root.load_store::<ContentDigest>()?;
196
197    let content_digest_diff = diff_content_digest(
198        output_snd,
199        xvc_root,
200        &stored_xvc_path_store,
201        &stored_xvc_metadata_store,
202        &stored_content_digest_store,
203        &stored_text_or_binary_store,
204        &xvc_path_diff,
205        &xvc_metadata_diff,
206        opts.text_or_binary,
207        Some(hash_algorithm),
208        !no_parallel,
209    );
210
211    update_store_records(xvc_root, &xvc_path_diff, true, false)?;
212    update_store_records(xvc_root, &xvc_metadata_diff, true, false)?;
213    update_store_records(xvc_root, &recheck_method_diff, true, false)?;
214    update_store_records(xvc_root, &text_or_binary_diff, true, false)?;
215    update_store_records(xvc_root, &content_digest_diff, true, false)?;
216
217    let file_targets: Vec<XvcPath> = targets
218        .iter()
219        .filter_map(|(xp, xmd)| {
220            if xmd.file_type == XvcFileType::File {
221                Some(xp.clone())
222            } else {
223                None
224            }
225        })
226        .collect();
227
228    // Warning: This one uses `opts.targets` instead of `targets` because
229    // `targets` has been filtered to only include files.
230    let dir_targets: Vec<XvcPath> = opts
231        .targets
232        .unwrap_or_else(|| vec![current_dir.to_string()])
233        .iter()
234        .filter_map(|t| {
235            let p = PathBuf::from(t);
236            if p.is_dir() {
237                XvcPath::new(xvc_root, current_dir, &p).ok()
238            } else {
239                None
240            }
241        })
242        .collect();
243
244    let current_gitignore = build_gitignore(xvc_root)?;
245
246    update_dir_gitignores(xvc_root, &current_gitignore, &dir_targets)?;
247    // We reload gitignores here to make sure we ignore the given dirs
248
249    let current_gitignore = build_gitignore(xvc_root)?;
250
251    update_file_gitignores(xvc_root, &current_gitignore, &file_targets)?;
252
253    if !opts.no_commit {
254        let current_xvc_path_store = xvc_root.load_store::<XvcPath>()?;
255
256        let updated_content_digest_store: HStore<ContentDigest> = content_digest_diff
257            .into_iter()
258            .filter_map(|(xe, cdd)| match cdd {
259                xvc_core::Diff::Identical => None,
260                xvc_core::Diff::RecordMissing { actual } => Some((xe, actual)),
261                xvc_core::Diff::ActualMissing { .. } => None,
262                xvc_core::Diff::Different { actual, .. } => Some((xe, actual)),
263                xvc_core::Diff::Skipped => None,
264            })
265            .collect();
266
267        // Only the updated paths are carried in
268        // TODO: Allow --force option to carry-in all paths
269        let xvc_paths_to_carry =
270            current_xvc_path_store.subset(updated_content_digest_store.keys().cloned())?;
271
272        // Filter directories from carry_in / recheck
273        let xvc_paths_to_carry: HStore<XvcPath> = xvc_paths_to_carry
274            .into_iter()
275            .filter_map(|(xe, xp)| {
276                targets.get(&xp).and_then(|xmd| {
277                    if xmd.file_type == XvcFileType::File {
278                        Some((xe, xp))
279                    } else {
280                        None
281                    }
282                })
283            })
284            .collect();
285
286        let cache_paths = updated_content_digest_store
287            .iter()
288            .filter_map(|(xe, cd)| {
289                xvc_paths_to_carry
290                    .get(xe)
291                    .and_then(|xp| XvcCachePath::new(xp, cd).ok())
292                    .map(|cp| (*xe, cp))
293            })
294            .collect();
295
296        let recheck_method_store = xvc_root.load_store::<RecheckMethod>()?;
297
298        carry_in(
299            output_snd,
300            xvc_root,
301            &xvc_paths_to_carry,
302            &cache_paths,
303            &recheck_method_store,
304            !no_parallel,
305            opts.force,
306        )?;
307    }
308
309    Ok(())
310}