Skip to main content

mkit_cli/commands/
sparse_checkout.rs

1//! `mkit sparse-checkout set|list|disable|reapply` — manage the sparse
2//! checkout pattern set at `.mkit/sparse-checkout`. Pattern parsing +
3//! tree-materialisation live in `mkit_core::ops::restore`.
4
5use std::fs;
6use std::io::Write;
7
8use clap::{Parser, Subcommand};
9use mkit_core::layout::RepoLayout;
10use mkit_core::object::Object;
11use mkit_core::ops::restore::{
12    self, RestoreOptions, load_sparse_checkout, parse_sparse_patterns, write_sparse_checkout,
13};
14use mkit_core::refs;
15use mkit_core::store::ObjectStore;
16
17use crate::clap_shim;
18use crate::exit;
19
20#[derive(Debug, Parser)]
21#[command(
22    name = "mkit sparse-checkout",
23    about = "Manage sparse-checkout patterns."
24)]
25struct SparseOpts {
26    #[command(subcommand)]
27    sub: Option<SparseCmd>,
28}
29
30#[derive(Debug, Subcommand)]
31enum SparseCmd {
32    /// List current patterns.
33    List,
34    /// Replace the pattern set and re-materialize.
35    Set {
36        /// One or more patterns.
37        #[arg(required = true)]
38        patterns: Vec<String>,
39    },
40    /// Drop patterns and re-materialize the full worktree.
41    Disable,
42    /// Re-apply the current patterns to the worktree.
43    Reapply,
44}
45
46#[must_use]
47pub fn run(args: &[String]) -> u8 {
48    let opts = match clap_shim::parse::<SparseOpts>("mkit sparse-checkout", args) {
49        Ok(o) => o,
50        Err(code) => return code,
51    };
52    let cwd = match std::env::current_dir() {
53        Ok(p) => p,
54        Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
55    };
56    let layout = match super::resolve_layout(&cwd) {
57        Ok(layout) => layout,
58        Err(code) => return code,
59    };
60    match opts.sub.unwrap_or(SparseCmd::List) {
61        SparseCmd::List => list_patterns(&layout),
62        SparseCmd::Set { patterns } => {
63            let joined = patterns.join("\n");
64            let opts = RestoreOptions {
65                clean: true,
66                sparse_patterns: Some(parse_sparse_patterns(&joined)),
67            };
68            apply_sparse_change(&layout, &opts, || {
69                let pat_refs: Vec<&str> = patterns.iter().map(String::as_str).collect();
70                write_sparse_checkout(&layout, &pat_refs)
71                    .map_err(|e| (format!("write sparse-checkout: {e}"), exit::CANTCREAT))
72            })
73        }
74        SparseCmd::Disable => disable(&layout),
75        SparseCmd::Reapply => reapply(&layout),
76    }
77}
78
79fn list_patterns(layout: &RepoLayout) -> u8 {
80    match load_sparse_checkout(layout) {
81        Ok(Some(pats)) => {
82            let mut stdout = std::io::stdout().lock();
83            for p in pats {
84                let neg = if p.negated { "!" } else { "" };
85                let slash = if p.dir_only { "/" } else { "" };
86                let _ = writeln!(stdout, "{neg}{}{slash}", p.pattern);
87            }
88            exit::OK
89        }
90        Ok(None) => {
91            // Empty listing → empty stdout; the human note goes to stderr.
92            let mut stderr = std::io::stderr().lock();
93            let _ = writeln!(stderr, "(no sparse-checkout patterns)");
94            exit::OK
95        }
96        Err(e) => emit_err(&format!("load sparse-checkout: {e}"), exit::GENERAL_ERROR),
97    }
98}
99
100fn disable(layout: &RepoLayout) -> u8 {
101    let opts = RestoreOptions {
102        clean: true,
103        sparse_patterns: None,
104    };
105    apply_sparse_change(layout, &opts, || {
106        let path = layout.sparse_checkout_file();
107        match fs::remove_file(&path) {
108            Ok(()) => Ok(()),
109            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
110            Err(e) => Err((format!("remove: {e}"), exit::GENERAL_ERROR)),
111        }
112    })
113}
114
115fn reapply(layout: &RepoLayout) -> u8 {
116    let sparse = match load_sparse_checkout(layout) {
117        Ok(s) => s,
118        Err(e) => return emit_err(&format!("load sparse-checkout: {e}"), exit::GENERAL_ERROR),
119    };
120    let opts = RestoreOptions {
121        clean: true,
122        sparse_patterns: sparse,
123    };
124    apply_sparse_change(layout, &opts, || Ok(()))
125}
126
127fn apply_sparse_change<F>(layout: &RepoLayout, opts: &RestoreOptions, mutate_config: F) -> u8
128where
129    F: FnOnce() -> Result<(), (String, u8)>,
130{
131    let store = match ObjectStore::open(layout) {
132        Ok(s) => s,
133        Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
134    };
135    let _lock = match super::acquire_worktree_lock(layout) {
136        Ok(lock) => lock,
137        Err(code) => return code,
138    };
139    let head = match refs::resolve_head(layout) {
140        Ok(Some(h)) => h,
141        Ok(None) => {
142            if let Err((msg, code)) = mutate_config() {
143                return emit_err(&msg, code);
144            }
145            let mut stderr = std::io::stderr().lock();
146            let _ = writeln!(stderr, "sparse-checkout applied");
147            return exit::OK;
148        }
149        Err(e) => return emit_err(&format!("resolve HEAD: {e}"), exit::GENERAL_ERROR),
150    };
151    let tree_hash = match store.read_object(&head) {
152        Ok(Object::Commit(c)) => c.tree_hash,
153        Ok(_) => return emit_err("HEAD is not a commit", exit::DATAERR),
154        Err(e) => return emit_err(&format!("read HEAD: {e}"), exit::GENERAL_ERROR),
155    };
156    if let Err(e) = super::ensure_restore_safe_with_options(layout, &store, tree_hash, opts) {
157        return emit_err(&e, exit::GENERAL_ERROR);
158    }
159    if let Err((msg, code)) = mutate_config() {
160        return emit_err(&msg, code);
161    }
162    match restore::restore_tree_to_worktree(&store, &tree_hash, layout.worktree_root(), opts) {
163        Ok(_) => {
164            let mut stderr = std::io::stderr().lock();
165            let _ = writeln!(stderr, "sparse-checkout applied");
166            exit::OK
167        }
168        Err(e) => emit_err(&format!("restore: {e}"), exit::GENERAL_ERROR),
169    }
170}
171
172use super::error as emit_err;