gitoxide_core/repository/
status.rs

1use 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    /// display all information about submodules, including ref changes, modifications and untracked files.
11    All,
12    /// Compare only the configuration of the superprojects commit with the actually checked out `HEAD` commit.
13    RefChange,
14    /// See if there are worktree modifications compared to the index, but do not check for untracked files.
15    Modifications,
16    /// Ignore all submodule changes.
17    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: _, // TODO: how to visualize copies?
174                ..
175            }) => {
176                // TODO: handle multi-status characters, there can also be modifications at the same time as determined by their ID and potentially diffstats.
177                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    // Known status letters: https://github.com/git/git/blob/6807fcfedab84bc8cd0fbf721bc13c4e68cda9ae/diff.h#L613
250    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}