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