Skip to main content

cargo_fixit/util/
vcs.rs

1use std::env;
2
3use anyhow::bail;
4use clap::Parser;
5
6use crate::CargoResult;
7
8#[derive(Parser, Debug)]
9pub struct VcsOpts {
10    /// Fix code even if a VCS was not detected
11    #[arg(long)]
12    pub allow_no_vcs: bool,
13
14    /// Fix code even if the working directory is dirty or has staged changes
15    #[arg(long)]
16    pub allow_dirty: bool,
17
18    /// Fix code even if the working directory has staged changes
19    #[arg(long)]
20    pub allow_staged: bool,
21}
22
23impl VcsOpts {
24    pub fn valid_vcs(&self) -> CargoResult<()> {
25        if self.allow_no_vcs {
26            return Ok(());
27        }
28        let cwd = env::current_dir()?;
29
30        let repo = git2::Repository::discover(&cwd).ok().filter(|r| {
31            if r.workdir().is_some_and(|workdir| workdir == cwd) {
32                true
33            } else {
34                !r.is_path_ignored(cwd).unwrap_or(false)
35            }
36        });
37
38        let Some(repo) = repo else {
39            bail!(
40                "no VCS found for this package and `cargo fix` can potentially \
41                perform destructive changes; if you'd like to suppress this \
42                error pass `--allow-no-vcs`"
43            );
44        };
45
46        if self.allow_staged && self.allow_dirty {
47            return Ok(());
48        }
49        let mut dirty_files = Vec::new();
50        let mut staged_files = Vec::new();
51
52        let mut repo_opts = git2::StatusOptions::new();
53        repo_opts.include_ignored(false);
54        repo_opts.include_untracked(true);
55        if self.allow_dirty {
56            repo_opts.show(git2::StatusShow::Index);
57        } else if self.allow_staged {
58            repo_opts.show(git2::StatusShow::Workdir);
59        }
60        for status in repo.statuses(Some(&mut repo_opts))?.iter() {
61            if let Some(path) = status.path() {
62                match status.status() {
63                    git2::Status::CURRENT => (),
64                    git2::Status::INDEX_NEW
65                        if repo
66                            .index()?
67                            .get_path(path.as_ref(), 0)
68                            .is_some_and(|entry| {
69                                git2::IndexEntryExtendedFlag::from_bits_truncate(
70                                    entry.flags_extended,
71                                )
72                                .is_intent_to_add()
73                            }) =>
74                    {
75                        if !self.allow_dirty {
76                            dirty_files.push(path.to_owned());
77                        }
78                    }
79                    git2::Status::INDEX_NEW
80                    | git2::Status::INDEX_MODIFIED
81                    | git2::Status::INDEX_DELETED
82                    | git2::Status::INDEX_RENAMED
83                    | git2::Status::INDEX_TYPECHANGE => {
84                        if !self.allow_staged {
85                            staged_files.push(path.to_owned());
86                        }
87                    }
88                    _ => {
89                        if !self.allow_dirty {
90                            dirty_files.push(path.to_owned());
91                        }
92                    }
93                };
94            }
95        }
96
97        if dirty_files.is_empty() && staged_files.is_empty() {
98            return Ok(());
99        }
100
101        let mut files_list = String::new();
102        for file in dirty_files {
103            files_list.push_str("  * ");
104            files_list.push_str(&file);
105            files_list.push_str(" (dirty)\n");
106        }
107        for file in staged_files {
108            files_list.push_str("  * ");
109            files_list.push_str(&file);
110            files_list.push_str(" (staged)\n");
111        }
112
113        bail!(
114            "the working directory of this package has uncommitted changes, and \
115            `cargo fix` can potentially perform destructive changes; if you'd \
116            like to suppress this error pass `--allow-dirty`, \
117            or commit the changes to these files:\n\
118            \n\
119            {files_list}\n\
120            "
121        );
122    }
123}