mkit_cli/commands/
ls_files.rs1use std::io::Write;
12use std::path::Path;
13
14use clap::Parser;
15use mkit_core::ignore::{self, IgnoreList};
16use mkit_core::index::{self, EntryStatus};
17use mkit_core::store::ObjectStore;
18
19use crate::clap_shim;
20use crate::exit;
21use crate::format;
22
23#[derive(Debug, Parser)]
24#[command(name = "mkit ls-files", about = "List tracked or untracked files.")]
25#[allow(clippy::struct_excessive_bools)] struct LsFilesOpts {
27 #[arg(short = 's', long = "stage")]
29 stage: bool,
30 #[arg(short = 'z')]
32 z: bool,
33 #[arg(long)]
35 others: bool,
36 #[arg(long = "exclude-standard")]
38 exclude_standard: bool,
39 #[arg(long)]
41 ignored: bool,
42}
43
44#[must_use]
45pub fn run(args: &[String]) -> u8 {
46 let opts = match clap_shim::parse::<LsFilesOpts>("mkit ls-files", args) {
47 Ok(o) => o,
48 Err(code) => return code,
49 };
50 let cwd = match std::env::current_dir() {
51 Ok(p) => p,
52 Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
53 };
54 let store = match ObjectStore::open(&cwd) {
55 Ok(s) => s,
56 Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
57 };
58 let idx = match super::read_or_seed_index_from_head(&cwd, &store) {
59 Ok(i) => i,
60 Err(e) => return emit_err(&e, exit::GENERAL_ERROR),
61 };
62
63 if opts.ignored && !opts.others {
67 return super::usage_error("mkit ls-files --ignored must be used with --others");
68 }
69
70 let mut stdout = std::io::stdout().lock();
71 let sep = if opts.z { '\0' } else { '\n' };
72
73 if opts.others {
74 let ignore = match ignore::load(&cwd) {
75 Ok(i) => i,
76 Err(e) => return emit_err(&format!("read ignore file: {e}"), exit::GENERAL_ERROR),
77 };
78 let mut others: Vec<String> = Vec::new();
79 if let Err(e) = collect_others(&cwd, &cwd, "", false, &idx, &ignore, &opts, &mut others) {
80 return emit_err(&format!("scan worktree: {e}"), exit::GENERAL_ERROR);
81 }
82 others.sort();
83 for path in &others {
84 write_path(&mut stdout, path, opts.z, sep);
85 }
86 return exit::OK;
87 }
88
89 let mut entries: Vec<&index::IndexEntry> = idx
91 .entries
92 .iter()
93 .filter(|e| e.status != EntryStatus::Removed)
94 .collect();
95 entries.sort_by(|a, b| a.path.cmp(&b.path));
96 for e in entries {
97 if opts.stage {
98 let mode = git_mode(e.status);
99 let _ = write!(
102 stdout,
103 "{mode} {} 0\t{}{sep}",
104 format::hex_hash(&e.object_hash),
105 shown_path(&e.path, opts.z)
106 );
107 } else {
108 write_path(&mut stdout, &e.path, opts.z, sep);
109 }
110 }
111 exit::OK
112}
113
114fn shown_path(path: &str, z: bool) -> std::borrow::Cow<'_, str> {
117 if z {
118 std::borrow::Cow::Borrowed(path)
119 } else {
120 match super::c_quote_path(path) {
121 Some(q) => std::borrow::Cow::Owned(q),
122 None => std::borrow::Cow::Borrowed(path),
123 }
124 }
125}
126
127fn write_path(out: &mut impl Write, path: &str, z: bool, sep: char) {
128 let _ = write!(out, "{}{sep}", shown_path(path, z));
129}
130
131fn git_mode(status: EntryStatus) -> &'static str {
133 match status {
134 EntryStatus::Executable => "100755",
135 EntryStatus::Symlink => "120000",
136 _ => "100644",
137 }
138}
139
140fn collect_others(
149 root: &Path,
150 dir: &Path,
151 prefix: &str,
152 parent_ignored: bool,
153 idx: &index::Index,
154 ignore: &IgnoreList,
155 opts: &LsFilesOpts,
156 out: &mut Vec<String>,
157) -> std::io::Result<()> {
158 let read = match std::fs::read_dir(dir) {
159 Ok(r) => r,
160 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
161 Err(e) => return Err(e),
162 };
163 for entry in read {
164 let entry = entry?;
165 let name = entry.file_name();
166 let Some(name) = name.to_str() else { continue };
167 if name.eq_ignore_ascii_case(".mkit") || name.eq_ignore_ascii_case(".git") {
168 continue;
169 }
170 let path = if prefix.is_empty() {
171 name.to_string()
172 } else {
173 format!("{prefix}/{name}")
174 };
175 let abs = root.join(&path);
176 let is_dir = std::fs::symlink_metadata(&abs)?.is_dir();
177 let entry_ignored = parent_ignored || ignore.is_ignored(&path, is_dir);
178 if is_dir {
179 collect_others(root, &abs, &path, entry_ignored, idx, ignore, opts, out)?;
184 continue;
185 }
186 if super::index_tracks_path_or_descendant(idx, &path) {
188 continue;
189 }
190 let include = if opts.ignored {
191 entry_ignored } else if opts.exclude_standard {
193 !entry_ignored } else {
195 true };
197 if include {
198 out.push(path);
199 }
200 }
201 Ok(())
202}
203
204fn emit_err(msg: &str, code: u8) -> u8 {
205 let mut stderr = std::io::stderr().lock();
206 let _ = writeln!(stderr, "error: {msg}");
207 code
208}