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