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 layout = match super::resolve_layout(&cwd) {
55 Ok(layout) => layout,
56 Err(code) => return code,
57 };
58 let store = match ObjectStore::open(&layout) {
59 Ok(s) => s,
60 Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
61 };
62 let idx = match super::read_or_seed_index_from_head(&layout, &store) {
63 Ok(i) => i,
64 Err(e) => return emit_err(&e, exit::GENERAL_ERROR),
65 };
66
67 if opts.ignored && !opts.others {
71 return super::usage_error("mkit ls-files --ignored must be used with --others");
72 }
73
74 let mut stdout = std::io::stdout().lock();
75 let sep = if opts.z { '\0' } else { '\n' };
76
77 if opts.others {
78 let ignore = match ignore::load(&cwd) {
79 Ok(i) => i,
80 Err(e) => return emit_err(&format!("read ignore file: {e}"), exit::GENERAL_ERROR),
81 };
82 let mut others: Vec<String> = Vec::new();
83 if let Err(e) = collect_others(&cwd, &cwd, "", false, &idx, &ignore, &opts, &mut others) {
84 return emit_err(&format!("scan worktree: {e}"), exit::GENERAL_ERROR);
85 }
86 others.sort();
87 for path in &others {
88 write_path(&mut stdout, path, opts.z, sep);
89 }
90 return exit::OK;
91 }
92
93 let mut entries: Vec<&index::IndexEntry> = idx
95 .entries
96 .iter()
97 .filter(|e| e.status != EntryStatus::Removed)
98 .collect();
99 entries.sort_by(|a, b| a.path.cmp(&b.path));
100 for e in entries {
101 if opts.stage {
102 let mode = git_mode(e.status);
103 let _ = write!(
106 stdout,
107 "{mode} {} 0\t{}{sep}",
108 format::hex_hash(&e.object_hash),
109 shown_path(&e.path, opts.z)
110 );
111 } else {
112 write_path(&mut stdout, &e.path, opts.z, sep);
113 }
114 }
115 exit::OK
116}
117
118fn shown_path(path: &str, z: bool) -> std::borrow::Cow<'_, str> {
121 if z {
122 std::borrow::Cow::Borrowed(path)
123 } else {
124 match super::c_quote_path(path) {
125 Some(q) => std::borrow::Cow::Owned(q),
126 None => std::borrow::Cow::Borrowed(path),
127 }
128 }
129}
130
131fn write_path(out: &mut impl Write, path: &str, z: bool, sep: char) {
132 let _ = write!(out, "{}{sep}", shown_path(path, z));
133}
134
135fn git_mode(status: EntryStatus) -> &'static str {
137 match status {
138 EntryStatus::Executable => "100755",
139 EntryStatus::Symlink => "120000",
140 _ => "100644",
141 }
142}
143
144fn collect_others(
153 root: &Path,
154 dir: &Path,
155 prefix: &str,
156 parent_ignored: bool,
157 idx: &index::Index,
158 ignore: &IgnoreList,
159 opts: &LsFilesOpts,
160 out: &mut Vec<String>,
161) -> std::io::Result<()> {
162 let read = match std::fs::read_dir(dir) {
163 Ok(r) => r,
164 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
165 Err(e) => return Err(e),
166 };
167 for entry in read {
168 let entry = entry?;
169 let name = entry.file_name();
170 let Some(name) = name.to_str() else { continue };
171 if name.eq_ignore_ascii_case(".mkit") || name.eq_ignore_ascii_case(".git") {
172 continue;
173 }
174 let path = if prefix.is_empty() {
175 name.to_string()
176 } else {
177 format!("{prefix}/{name}")
178 };
179 let abs = root.join(&path);
180 let is_dir = std::fs::symlink_metadata(&abs)?.is_dir();
181 let entry_ignored = parent_ignored || ignore.is_ignored(&path, is_dir);
182 if is_dir {
183 collect_others(root, &abs, &path, entry_ignored, idx, ignore, opts, out)?;
188 continue;
189 }
190 if super::index_tracks_path_or_descendant(idx, &path) {
192 continue;
193 }
194 let include = if opts.ignored {
195 entry_ignored } else if opts.exclude_standard {
197 !entry_ignored } else {
199 true };
201 if include {
202 out.push(path);
203 }
204 }
205 Ok(())
206}
207
208use super::error as emit_err;