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