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