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        }
58        for status in repo.statuses(Some(&mut repo_opts))?.iter() {
59            if let Some(path) = status.path() {
60                match status.status() {
61                    git2::Status::CURRENT => (),
62                    git2::Status::INDEX_NEW
63                        if repo
64                            .index()?
65                            .get_path(path.as_ref(), 0)
66                            .is_some_and(|entry| {
67                                git2::IndexEntryExtendedFlag::from_bits_truncate(
68                                    entry.flags_extended,
69                                )
70                                .is_intent_to_add()
71                            }) =>
72                    {
73                        if !self.allow_dirty {
74                            dirty_files.push(path.to_owned());
75                        }
76                    }
77                    git2::Status::INDEX_NEW
78                    | git2::Status::INDEX_MODIFIED
79                    | git2::Status::INDEX_DELETED
80                    | git2::Status::INDEX_RENAMED
81                    | git2::Status::INDEX_TYPECHANGE => {
82                        if !self.allow_staged {
83                            staged_files.push(path.to_owned());
84                        }
85                    }
86                    _ => {
87                        if !self.allow_dirty {
88                            dirty_files.push(path.to_owned());
89                        }
90                    }
91                };
92            }
93        }
94
95        if dirty_files.is_empty() && staged_files.is_empty() {
96            return Ok(());
97        }
98
99        let mut files_list = String::new();
100        for file in dirty_files {
101            files_list.push_str("  * ");
102            files_list.push_str(&file);
103            files_list.push_str(" (dirty)\n");
104        }
105        for file in staged_files {
106            files_list.push_str("  * ");
107            files_list.push_str(&file);
108            files_list.push_str(" (staged)\n");
109        }
110
111        bail!(
112            "the working directory of this package has uncommitted changes, and \
113            `cargo fix` can potentially perform destructive changes; if you'd \
114            like to suppress this error pass `--allow-dirty`, \
115            or commit the changes to these files:\n\
116            \n\
117            {files_list}\n\
118            "
119        );
120    }
121}