@@ -70,14 +70,11 @@ pub enum PathFreshness {
7070/// were not modified upstream in the meantime. In that case we would be redownloading CI
7171/// artifacts unnecessarily.
7272///
73- /// - In CI, we use a shallow clone of depth 2, i.e., we fetch only a single parent commit
74- /// (which will be the most recent bors merge commit) and do not have access
75- /// to the full git history. Luckily, we only need to distinguish between two situations:
76- /// 1) The current PR made modifications to `target_paths`.
77- /// In that case, a build is typically necessary.
78- /// 2) The current PR did not make modifications to `target_paths`.
79- /// In that case we simply take the latest upstream commit, because on CI there is no need to avoid
80- /// redownloading.
73+ /// - In CI, we prefer a shallow merge-parent fast path when `HEAD` is a CI-generated merge
74+ /// commit. However, fork push workflows can also run in shallow clones where `HEAD` is just the
75+ /// branch tip, so blindly using `HEAD^1` there would pick a fork commit instead of the upstream
76+ /// base. In those cases we fall back to the fetched nightly branch ref, and only then to the
77+ /// normal upstream search logic.
8178pub fn check_path_modifications (
8279 git_dir : & Path ,
8380 config : & GitConfig < ' _ > ,
@@ -90,24 +87,9 @@ pub fn check_path_modifications(
9087 }
9188
9289 let upstream_sha = if matches ! ( ci_env, CiEnv :: GitHubActions ) {
93- // Here the situation is different for PR CI and try/auto CI.
94- // For PR CI, we have the following history:
95- // <merge commit made by GitHub>
96- // 1-N PR commits
97- // upstream merge commit made by bors
98- //
99- // For try/auto CI, we have the following history:
100- // <**non-upstream** merge commit made by bors>
101- // 1-N PR commits
102- // upstream merge commit made by bors
103- //
104- // But on both cases, HEAD should be a merge commit.
105- // So if HEAD contains modifications of `target_paths`, our PR has modified
106- // them. If not, we can use the only available upstream commit for downloading
107- // artifacts.
108-
109- // Do not include HEAD, as it is never an upstream commit
110- // If we do not find an upstream commit in CI, something is seriously wrong.
90+ // CI may be running on a synthetic merge ref or a shallow fork push ref.
91+ // `get_closest_upstream_commit` handles the trusted merge-parent fast path and falls back
92+ // to the fetched nightly branch ref when the merge-parent assumption is not valid.
11193 Some (
11294 get_closest_upstream_commit ( Some ( git_dir) , config, ci_env) ?
11395 . expect ( "No upstream commit was found on CI" ) ,
@@ -224,23 +206,45 @@ fn get_latest_upstream_commit_that_modified_files(
224206/// Returns the most recent (ordered chronologically) commit found in the local history that
225207/// should exist upstream. We identify upstream commits by the e-mail of the commit
226208/// author.
227- ///
228- /// If we are in CI, we simply return our first parent.
229209pub fn get_closest_upstream_commit (
230210 git_dir : Option < & Path > ,
231211 config : & GitConfig < ' _ > ,
232212 env : CiEnv ,
233213) -> Result < Option < String > , String > {
234- let base = match env {
235- CiEnv :: None => "HEAD" ,
214+ match env {
215+ CiEnv :: None => get_closest_upstream_commit_from_ref ( git_dir , config , "HEAD" ) ,
236216 CiEnv :: GitHubActions => {
237- // On CI, we should always have a non-upstream merge commit at the tip,
238- // and our first parent should be the most recently merged upstream commit.
239- // We thus simply return our first parent.
240- return resolve_commit_sha ( git_dir, "HEAD^1" ) . map ( Some ) ;
217+ // CI-generated PR and auto-merge refs put a synthetic merge commit at HEAD, so the
218+ // first parent is usually the most recent upstream merge commit. Fork push workflows
219+ // do not have that shape, though, and in shallow clones `HEAD^1` can just be the
220+ // previous fork commit. Only trust the fast path when it points at an actual upstream
221+ // merge-bot commit, otherwise fall back to the fetched nightly branch.
222+ if is_merge_commit ( git_dir, "HEAD" ) ? {
223+ let parent = resolve_commit_sha ( git_dir, "HEAD^1" ) ?;
224+ if is_upstream_merge_commit ( git_dir, & parent, config) ? {
225+ return Ok ( Some ( parent) ) ;
226+ }
227+ }
228+
229+ let nightly_ref = format ! ( "refs/remotes/origin/{}" , config. nightly_branch) ;
230+ if git_ref_exists ( git_dir, & nightly_ref) ? {
231+ if let Some ( upstream) =
232+ get_closest_upstream_commit_from_ref ( git_dir, config, & nightly_ref) ?
233+ {
234+ return Ok ( Some ( upstream) ) ;
235+ }
236+ }
237+
238+ get_closest_upstream_commit_from_ref ( git_dir, config, "HEAD" )
241239 }
242- } ;
240+ }
241+ }
243242
243+ fn get_closest_upstream_commit_from_ref (
244+ git_dir : Option < & Path > ,
245+ config : & GitConfig < ' _ > ,
246+ base : & str ,
247+ ) -> Result < Option < String > , String > {
244248 let mut git = Command :: new ( "git" ) ;
245249
246250 if let Some ( git_dir) = git_dir {
@@ -272,6 +276,60 @@ pub fn get_closest_upstream_commit(
272276 if output. is_empty ( ) { Ok ( None ) } else { Ok ( Some ( output) ) }
273277}
274278
279+ fn is_merge_commit ( git_dir : Option < & Path > , commit_ref : & str ) -> Result < bool , String > {
280+ let mut git = Command :: new ( "git" ) ;
281+ if let Some ( git_dir) = git_dir {
282+ git. current_dir ( git_dir) ;
283+ }
284+
285+ let output = git
286+ . args ( [ "rev-parse" , "--verify" , "--quiet" , & format ! ( "{commit_ref}^2" ) ] )
287+ . stderr ( Stdio :: null ( ) )
288+ . stdout ( Stdio :: null ( ) )
289+ . status ( )
290+ . map_err ( |e| format ! ( "failed to run command: {git:?}: {e}" ) ) ?;
291+ Ok ( output. success ( ) )
292+ }
293+
294+ fn git_ref_exists ( git_dir : Option < & Path > , refname : & str ) -> Result < bool , String > {
295+ let mut git = Command :: new ( "git" ) ;
296+ if let Some ( git_dir) = git_dir {
297+ git. current_dir ( git_dir) ;
298+ }
299+
300+ let output = git
301+ . args ( [ "rev-parse" , "--verify" , "--quiet" , refname] )
302+ . stderr ( Stdio :: null ( ) )
303+ . stdout ( Stdio :: null ( ) )
304+ . status ( )
305+ . map_err ( |e| format ! ( "failed to run command: {git:?}: {e}" ) ) ?;
306+ Ok ( output. success ( ) )
307+ }
308+
309+ fn is_upstream_merge_commit (
310+ git_dir : Option < & Path > ,
311+ commit_ref : & str ,
312+ config : & GitConfig < ' _ > ,
313+ ) -> Result < bool , String > {
314+ let mut git = Command :: new ( "git" ) ;
315+ if let Some ( git_dir) = git_dir {
316+ git. current_dir ( git_dir) ;
317+ }
318+
319+ git. args ( [ "show" , "-s" , "--format=%ae" , commit_ref] ) ;
320+ let author_email = output_result ( & mut git) ?. trim ( ) . to_owned ( ) ;
321+ let merge_bot_email = extract_author_email ( config. git_merge_commit_email ) ;
322+ Ok ( author_email == merge_bot_email || author_email == TEMPORARY_BORS_EMAIL )
323+ }
324+
325+ fn extract_author_email ( author : & str ) -> & str {
326+ author
327+ . split_once ( '<' )
328+ . and_then ( |( _, email) | email. trim ( ) . strip_suffix ( '>' ) )
329+ . map ( str:: trim)
330+ . unwrap_or_else ( || author. trim ( ) )
331+ }
332+
275333/// Resolve the commit SHA of `commit_ref`.
276334fn resolve_commit_sha ( git_dir : Option < & Path > , commit_ref : & str ) -> Result < String , String > {
277335 let mut git = Command :: new ( "git" ) ;
0 commit comments