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