gitoxide_core/repository/
status.rs1use std::path::Path;
2
3use anyhow::bail;
4use gix::{
5 bstr::{BStr, BString, ByteSlice},
6 status::{self, index_worktree},
7};
8use gix_status::index_as_worktree::{Change, Conflict, EntryStatus};
9
10use crate::OutputFormat;
11
12pub enum Submodules {
13 All,
15 RefChange,
17 Modifications,
19 None,
21}
22
23#[derive(Copy, Clone)]
24pub enum Ignored {
25 Collapsed,
26 Matching,
27}
28
29#[derive(Copy, Clone)]
30pub enum Format {
31 Simplified,
32 PorcelainV2,
33}
34
35pub struct Options {
36 pub ignored: Option<Ignored>,
37 pub format: Format,
38 pub output_format: OutputFormat,
39 pub submodules: Option<Submodules>,
40 pub thread_limit: Option<usize>,
41 pub statistics: bool,
42 pub allow_write: bool,
43 pub index_worktree_renames: Option<f32>,
44 pub untracked: Option<gix::status::UntrackedFiles>,
45}
46
47pub fn show(
48 repo: gix::Repository,
49 pathspecs: Vec<BString>,
50 mut out: impl std::io::Write,
51 mut err: impl std::io::Write,
52 mut progress: impl gix::NestedProgress + 'static,
53 Options {
54 ignored,
55 format,
56 output_format,
57 submodules,
58 thread_limit,
59 allow_write,
60 statistics,
61 index_worktree_renames,
62 untracked,
63 }: Options,
64) -> anyhow::Result<()> {
65 if output_format != OutputFormat::Human {
66 bail!("Only human format is supported right now");
67 }
68 if !matches!(format, Format::Simplified) {
69 bail!("Only the simplified format is currently implemented");
70 }
71
72 let start = std::time::Instant::now();
73 let prefix = repo.prefix()?.unwrap_or(Path::new(""));
74 let index_progress = progress.add_child("traverse index");
75 let mut status = repo
76 .status(index_progress)?
77 .should_interrupt_shared(&gix::interrupt::IS_INTERRUPTED);
78 if let Some(untracked) = untracked {
79 status = status.untracked_files(untracked);
80 }
81 let mut iter = status
82 .index_worktree_options_mut(|opts| {
83 if let Some((opts, ignored)) = opts.dirwalk_options.as_mut().zip(ignored) {
84 opts.set_emit_ignored(Some(match ignored {
85 Ignored::Collapsed => gix::dir::walk::EmissionMode::CollapseDirectory,
86 Ignored::Matching => gix::dir::walk::EmissionMode::Matching,
87 }));
88 }
89 opts.rewrites = index_worktree_renames.map(|percentage| gix::diff::Rewrites {
90 copies: None,
91 percentage: Some(percentage),
92 limit: 0,
93 track_empty: false,
94 });
95 if opts.rewrites.is_some() {
96 opts.dirwalk_options.iter_mut().for_each(|opts| {
97 opts.set_emit_untracked(gix::dir::walk::EmissionMode::Matching);
98 if ignored.is_some() {
99 opts.set_emit_ignored(Some(gix::dir::walk::EmissionMode::Matching));
100 }
101 });
102 }
103 opts.thread_limit = thread_limit;
104 opts.sorting = Some(gix::status::plumbing::index_as_worktree_with_renames::Sorting::ByPathCaseSensitive);
105 })
106 .index_worktree_submodules(match submodules {
107 Some(mode) => {
108 let ignore = match mode {
109 Submodules::All => gix::submodule::config::Ignore::None,
110 Submodules::RefChange => gix::submodule::config::Ignore::Dirty,
111 Submodules::Modifications => gix::submodule::config::Ignore::Untracked,
112 Submodules::None => gix::submodule::config::Ignore::All,
113 };
114 gix::status::Submodule::Given {
115 ignore,
116 check_dirty: false,
117 }
118 }
119 None => gix::status::Submodule::AsConfigured { check_dirty: false },
120 })
121 .into_iter(pathspecs)?;
122
123 for item in iter.by_ref() {
124 let item = item?;
125 match item {
126 status::Item::TreeIndex(change) => {
127 let (location, _, _, _) = change.fields();
128 let status = match change {
129 gix::diff::index::Change::Addition { .. } => "A",
130 gix::diff::index::Change::Deletion { .. } => "D",
131 gix::diff::index::Change::Modification { .. } => "M",
132 gix::diff::index::Change::Rewrite {
133 ref source_location, ..
134 } => {
135 let source_location = gix::path::from_bstr(source_location.as_ref());
136 let source_location = gix::path::relativize_with_prefix(&source_location, prefix);
137 writeln!(
138 out,
139 "{status: >2} {source_rela_path} → {dest_rela_path}",
140 status = "R",
141 source_rela_path = source_location.display(),
142 dest_rela_path =
143 gix::path::relativize_with_prefix(&gix::path::from_bstr(location), prefix).display(),
144 )?;
145 continue;
146 }
147 };
148 writeln!(
149 out,
150 "{status: >2} {rela_path}",
151 rela_path = gix::path::relativize_with_prefix(&gix::path::from_bstr(location), prefix).display(),
152 )?;
153 }
154 status::Item::IndexWorktree(index_worktree::Item::Modification {
155 entry: _,
156 entry_index: _,
157 rela_path,
158 status,
159 }) => print_index_entry_status(&mut out, prefix, rela_path.as_ref(), status)?,
160 status::Item::IndexWorktree(index_worktree::Item::DirectoryContents {
161 entry,
162 collapsed_directory_status,
163 }) => {
164 if collapsed_directory_status.is_none() {
165 writeln!(
166 out,
167 "{status: >3} {rela_path}{slash}",
168 status = "?",
169 rela_path =
170 gix::path::relativize_with_prefix(&gix::path::from_bstr(entry.rela_path), prefix).display(),
171 slash = if entry.disk_kind.unwrap_or(gix::dir::entry::Kind::File).is_dir() {
172 "/"
173 } else {
174 ""
175 }
176 )?;
177 }
178 }
179 status::Item::IndexWorktree(index_worktree::Item::Rewrite {
180 source, dirwalk_entry, ..
181 }) => {
182 writeln!(
184 out,
185 "{status: >3} {source_rela_path} → {dest_rela_path}",
186 status = "R",
187 source_rela_path =
188 gix::path::relativize_with_prefix(&gix::path::from_bstr(source.rela_path()), prefix).display(),
189 dest_rela_path = gix::path::relativize_with_prefix(
190 &gix::path::from_bstr(dirwalk_entry.rela_path.as_bstr()),
191 prefix
192 )
193 .display(),
194 )?;
195 }
196 }
197 }
198 if gix::interrupt::is_triggered() {
199 bail!("interrupted by user");
200 }
201
202 let out = iter.outcome_mut().expect("successful iteration has outcome");
203
204 if out.has_changes() && allow_write {
205 out.write_changes().transpose()?;
206 }
207
208 if statistics {
209 writeln!(err, "{outcome:#?}", outcome = out.index_worktree).ok();
210 }
211
212 progress.init(Some(out.worktree_index.entries().len()), gix::progress::count("files"));
213 progress.set(out.worktree_index.entries().len());
214 progress.show_throughput(start);
215 Ok(())
216}
217
218fn print_index_entry_status(
219 out: &mut dyn std::io::Write,
220 prefix: &Path,
221 rela_path: &BStr,
222 status: EntryStatus<(), gix::submodule::Status>,
223) -> std::io::Result<()> {
224 let char_storage;
225 let status = match status {
226 EntryStatus::Conflict { summary, entries: _ } => as_str(summary),
227 EntryStatus::Change(change) => {
228 char_storage = change_to_char(&change);
229 std::str::from_utf8(std::slice::from_ref(&char_storage)).expect("valid ASCII")
230 }
231 EntryStatus::NeedsUpdate(_stat) => {
232 return Ok(());
233 }
234 EntryStatus::IntentToAdd => "A",
235 };
236
237 let rela_path = gix::path::from_bstr(rela_path);
238 let display_path = gix::path::relativize_with_prefix(&rela_path, prefix);
239 writeln!(out, "{status: >3} {}", display_path.display())
240}
241
242fn as_str(c: Conflict) -> &'static str {
243 match c {
244 Conflict::BothDeleted => "DD",
245 Conflict::AddedByUs => "AU",
246 Conflict::DeletedByThem => "UD",
247 Conflict::AddedByThem => "UA",
248 Conflict::DeletedByUs => "DU",
249 Conflict::BothAdded => "AA",
250 Conflict::BothModified => "UU",
251 }
252}
253
254fn change_to_char(change: &Change<(), gix::submodule::Status>) -> u8 {
255 match change {
257 Change::Removed => b'D',
258 Change::Type { .. } => b'T',
259 Change::SubmoduleModification(_) => b'M',
260 Change::Modification {
261 executable_bit_changed, ..
262 } => {
263 if *executable_bit_changed {
264 b'X'
265 } else {
266 b'M'
267 }
268 }
269 }
270}