Skip to main content

gitoxide_core/repository/
status.rs

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