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}