mkit_cli/commands/
sparse_checkout.rs1use 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,
34 Set {
36 #[arg(required = true)]
38 patterns: Vec<String>,
39 },
40 Disable,
42 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 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;