1use 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,
33 Set {
35 #[arg(required = true)]
37 patterns: Vec<String>,
38 },
39 Disable,
41 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 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}