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}
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: _, // TODO: how to visualize copies?
177                ..
178            }) => {
179                // TODO: handle multi-status characters, there can also be modifications at the same time as determined by their ID and potentially diffstats.
180                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    // Known status letters: https://github.com/git/git/blob/6807fcfedab84bc8cd0fbf721bc13c4e68cda9ae/diff.h#L613
253    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}