build_helper/
git.rs
1use std::path::{Path, PathBuf};
2use std::process::{Command, Stdio};
3
4use crate::ci::CiEnv;
5
6pub struct GitConfig<'a> {
7 pub git_repository: &'a str,
8 pub nightly_branch: &'a str,
9 pub git_merge_commit_email: &'a str,
10}
11
12pub fn output_result(cmd: &mut Command) -> Result<String, String> {
14 let output = match cmd.stderr(Stdio::inherit()).output() {
15 Ok(status) => status,
16 Err(e) => return Err(format!("failed to run command: {:?}: {}", cmd, e)),
17 };
18 if !output.status.success() {
19 return Err(format!(
20 "command did not execute successfully: {:?}\n\
21 expected success, got: {}\n{}",
22 cmd,
23 output.status,
24 String::from_utf8(output.stderr).map_err(|err| format!("{err:?}"))?
25 ));
26 }
27 String::from_utf8(output.stdout).map_err(|err| format!("{err:?}"))
28}
29
30pub fn get_rust_lang_rust_remote(
39 config: &GitConfig<'_>,
40 git_dir: Option<&Path>,
41) -> Result<String, String> {
42 let mut git = Command::new("git");
43 if let Some(git_dir) = git_dir {
44 git.current_dir(git_dir);
45 }
46 git.args(["config", "--local", "--get-regex", "remote\\..*\\.url"]);
47 let stdout = output_result(&mut git)?;
48
49 let rust_lang_remote = stdout
50 .lines()
51 .find(|remote| remote.contains(config.git_repository))
52 .ok_or_else(|| format!("{} remote not found", config.git_repository))?;
53
54 let remote_name =
55 rust_lang_remote.split('.').nth(1).ok_or_else(|| "remote name not found".to_owned())?;
56 Ok(remote_name.into())
57}
58
59pub fn rev_exists(rev: &str, git_dir: Option<&Path>) -> Result<bool, String> {
60 let mut git = Command::new("git");
61 if let Some(git_dir) = git_dir {
62 git.current_dir(git_dir);
63 }
64 git.args(["rev-parse", rev]);
65 let output = git.output().map_err(|err| format!("{err:?}"))?;
66
67 match output.status.code() {
68 Some(0) => Ok(true),
69 Some(128) => Ok(false),
70 None => Err(format!(
71 "git didn't exit properly: {}",
72 String::from_utf8(output.stderr).map_err(|err| format!("{err:?}"))?
73 )),
74 Some(code) => Err(format!(
75 "git command exited with status code: {code}: {}",
76 String::from_utf8(output.stderr).map_err(|err| format!("{err:?}"))?
77 )),
78 }
79}
80
81pub fn updated_master_branch(
87 config: &GitConfig<'_>,
88 git_dir: Option<&Path>,
89) -> Result<String, String> {
90 let upstream_remote = get_rust_lang_rust_remote(config, git_dir)?;
91 let branch = config.nightly_branch;
92 for upstream_master in [format!("{upstream_remote}/{branch}"), format!("origin/{branch}")] {
93 if rev_exists(&upstream_master, git_dir)? {
94 return Ok(upstream_master);
95 }
96 }
97
98 Err("Cannot find any suitable upstream master branch".to_owned())
99}
100
101fn git_upstream_merge_base(
106 config: &GitConfig<'_>,
107 git_dir: Option<&Path>,
108) -> Result<String, String> {
109 let updated_master = updated_master_branch(config, git_dir)?;
110 let mut git = Command::new("git");
111 if let Some(git_dir) = git_dir {
112 git.current_dir(git_dir);
113 }
114 Ok(output_result(git.arg("merge-base").arg(&updated_master).arg("HEAD"))?.trim().to_owned())
115}
116
117pub fn get_closest_merge_commit(
122 git_dir: Option<&Path>,
123 config: &GitConfig<'_>,
124 target_paths: &[PathBuf],
125) -> Result<String, String> {
126 let mut git = Command::new("git");
127
128 if let Some(git_dir) = git_dir {
129 git.current_dir(git_dir);
130 }
131
132 let channel = include_str!("../../ci/channel");
133
134 let merge_base = {
135 if CiEnv::is_ci() &&
136 (channel == "nightly" || !CiEnv::is_rust_lang_managed_ci_job())
144 {
145 git_upstream_merge_base(config, git_dir).unwrap()
146 } else {
147 "HEAD".to_string()
150 }
151 };
152
153 git.args([
154 "rev-list",
155 &format!("--author={}", config.git_merge_commit_email),
156 "-n1",
157 "--first-parent",
158 &merge_base,
159 ]);
160
161 if !target_paths.is_empty() {
162 git.arg("--").args(target_paths);
163 }
164
165 Ok(output_result(&mut git)?.trim().to_owned())
166}
167
168pub fn get_git_modified_files(
173 config: &GitConfig<'_>,
174 git_dir: Option<&Path>,
175 extensions: &[&str],
176) -> Result<Option<Vec<String>>, String> {
177 let merge_base = get_closest_merge_commit(git_dir, config, &[])?;
178
179 let mut git = Command::new("git");
180 if let Some(git_dir) = git_dir {
181 git.current_dir(git_dir);
182 }
183 let files = output_result(git.args(["diff-index", "--name-status", merge_base.trim()]))?
184 .lines()
185 .filter_map(|f| {
186 let (status, name) = f.trim().split_once(char::is_whitespace).unwrap();
187 if status == "D" {
188 None
189 } else if Path::new(name).extension().map_or(false, |ext| {
190 extensions.is_empty() || extensions.contains(&ext.to_str().unwrap())
191 }) {
192 Some(name.to_owned())
193 } else {
194 None
195 }
196 })
197 .collect();
198 Ok(Some(files))
199}
200
201pub fn get_git_untracked_files(
203 config: &GitConfig<'_>,
204 git_dir: Option<&Path>,
205) -> Result<Option<Vec<String>>, String> {
206 let Ok(_updated_master) = updated_master_branch(config, git_dir) else {
207 return Ok(None);
208 };
209 let mut git = Command::new("git");
210 if let Some(git_dir) = git_dir {
211 git.current_dir(git_dir);
212 }
213
214 let files = output_result(git.arg("ls-files").arg("--others").arg("--exclude-standard"))?
215 .lines()
216 .map(|s| s.trim().to_owned())
217 .collect();
218 Ok(Some(files))
219}