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}