• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 use std::process::Stdio;
2 use std::{path::Path, process::Command};
3 
4 /// Runs a command and returns the output
output_result(cmd: &mut Command) -> Result<String, String>5 fn output_result(cmd: &mut Command) -> Result<String, String> {
6     let output = match cmd.stderr(Stdio::inherit()).output() {
7         Ok(status) => status,
8         Err(e) => return Err(format!("failed to run command: {:?}: {}", cmd, e)),
9     };
10     if !output.status.success() {
11         return Err(format!(
12             "command did not execute successfully: {:?}\n\
13              expected success, got: {}\n{}",
14             cmd,
15             output.status,
16             String::from_utf8(output.stderr).map_err(|err| format!("{err:?}"))?
17         ));
18     }
19     Ok(String::from_utf8(output.stdout).map_err(|err| format!("{err:?}"))?)
20 }
21 
22 /// Finds the remote for rust-lang/rust.
23 /// For example for these remotes it will return `upstream`.
24 /// ```text
25 /// origin  https://github.com/Nilstrieb/rust.git (fetch)
26 /// origin  https://github.com/Nilstrieb/rust.git (push)
27 /// upstream        https://github.com/rust-lang/rust (fetch)
28 /// upstream        https://github.com/rust-lang/rust (push)
29 /// ```
get_rust_lang_rust_remote(git_dir: Option<&Path>) -> Result<String, String>30 pub fn get_rust_lang_rust_remote(git_dir: Option<&Path>) -> Result<String, String> {
31     let mut git = Command::new("git");
32     if let Some(git_dir) = git_dir {
33         git.current_dir(git_dir);
34     }
35     git.args(["config", "--local", "--get-regex", "remote\\..*\\.url"]);
36     let stdout = output_result(&mut git)?;
37 
38     let rust_lang_remote = stdout
39         .lines()
40         .find(|remote| remote.contains("rust-lang"))
41         .ok_or_else(|| "rust-lang/rust remote not found".to_owned())?;
42 
43     let remote_name =
44         rust_lang_remote.split('.').nth(1).ok_or_else(|| "remote name not found".to_owned())?;
45     Ok(remote_name.into())
46 }
47 
rev_exists(rev: &str, git_dir: Option<&Path>) -> Result<bool, String>48 pub fn rev_exists(rev: &str, git_dir: Option<&Path>) -> Result<bool, String> {
49     let mut git = Command::new("git");
50     if let Some(git_dir) = git_dir {
51         git.current_dir(git_dir);
52     }
53     git.args(["rev-parse", rev]);
54     let output = git.output().map_err(|err| format!("{err:?}"))?;
55 
56     match output.status.code() {
57         Some(0) => Ok(true),
58         Some(128) => Ok(false),
59         None => {
60             return Err(format!(
61                 "git didn't exit properly: {}",
62                 String::from_utf8(output.stderr).map_err(|err| format!("{err:?}"))?
63             ));
64         }
65         Some(code) => {
66             return Err(format!(
67                 "git command exited with status code: {code}: {}",
68                 String::from_utf8(output.stderr).map_err(|err| format!("{err:?}"))?
69             ));
70         }
71     }
72 }
73 
74 /// Returns the master branch from which we can take diffs to see changes.
75 /// This will usually be rust-lang/rust master, but sometimes this might not exist.
76 /// This could be because the user is updating their forked master branch using the GitHub UI
77 /// and therefore doesn't need an upstream master branch checked out.
78 /// We will then fall back to origin/master in the hope that at least this exists.
updated_master_branch(git_dir: Option<&Path>) -> Result<String, String>79 pub fn updated_master_branch(git_dir: Option<&Path>) -> Result<String, String> {
80     let upstream_remote = get_rust_lang_rust_remote(git_dir)?;
81     let upstream_master = format!("{upstream_remote}/master");
82     if rev_exists(&upstream_master, git_dir)? {
83         return Ok(upstream_master);
84     }
85 
86     // We could implement smarter logic here in the future.
87     Ok("origin/master".into())
88 }
89 
90 /// Returns the files that have been modified in the current branch compared to the master branch.
91 /// The `extensions` parameter can be used to filter the files by their extension.
92 /// If `extensions` is empty, all files will be returned.
get_git_modified_files( git_dir: Option<&Path>, extensions: &Vec<&str>, ) -> Result<Option<Vec<String>>, String>93 pub fn get_git_modified_files(
94     git_dir: Option<&Path>,
95     extensions: &Vec<&str>,
96 ) -> Result<Option<Vec<String>>, String> {
97     let Ok(updated_master) = updated_master_branch(git_dir) else { return Ok(None); };
98 
99     let git = || {
100         let mut git = Command::new("git");
101         if let Some(git_dir) = git_dir {
102             git.current_dir(git_dir);
103         }
104         git
105     };
106 
107     let merge_base = output_result(git().arg("merge-base").arg(&updated_master).arg("HEAD"))?;
108     let files = output_result(git().arg("diff-index").arg("--name-only").arg(merge_base.trim()))?
109         .lines()
110         .map(|s| s.trim().to_owned())
111         .filter(|f| {
112             Path::new(f).extension().map_or(false, |ext| {
113                 extensions.is_empty() || extensions.contains(&ext.to_str().unwrap())
114             })
115         })
116         .collect();
117     Ok(Some(files))
118 }
119 
120 /// Returns the files that haven't been added to git yet.
get_git_untracked_files(git_dir: Option<&Path>) -> Result<Option<Vec<String>>, String>121 pub fn get_git_untracked_files(git_dir: Option<&Path>) -> Result<Option<Vec<String>>, String> {
122     let Ok(_updated_master) = updated_master_branch(git_dir) else { return Ok(None); };
123     let mut git = Command::new("git");
124     if let Some(git_dir) = git_dir {
125         git.current_dir(git_dir);
126     }
127 
128     let files = output_result(git.arg("ls-files").arg("--others").arg("--exclude-standard"))?
129         .lines()
130         .map(|s| s.trim().to_owned())
131         .collect();
132     Ok(Some(files))
133 }
134