git_assist/command/
bisect.rs

1use std::{
2    collections::HashSet,
3    os::unix::process::ExitStatusExt,
4    path::PathBuf,
5    process::{Command, ExitStatus},
6};
7
8use git2::{Oid, Repository as GitRepository};
9
10use crate::{
11    git::commits_in_range,
12    host::{GitHost, GitPullRequest, GitRepositoryUrl},
13};
14
15pub struct SkipPullRequestsConfig {
16    /// The git repository.
17    pub repository: GitRepositoryUrl,
18
19    // The git directory.
20    pub directory: PathBuf,
21
22    /// A known "good" git commit.
23    pub good: String,
24
25    /// A known "bad" git commit.
26    pub bad: String,
27
28    /// Perform a "dry" run.
29    pub dry_run: bool,
30}
31
32pub async fn skip_pull_requests(
33    host: &dyn GitHost,
34    config: &SkipPullRequestsConfig,
35) -> anyhow::Result<ExitStatus> {
36    eprintln!("Opening git repository ...");
37    let repository = GitRepository::open(&config.directory)?;
38
39    let range_commit_ids: HashSet<Oid> = {
40        let range = (
41            repository.revparse_single(&config.good)?.id(),
42            repository.revparse_single(&config.bad)?.id(),
43        );
44        commits_in_range(&repository, range)?
45            .into_iter()
46            .map(|commit| commit.id())
47            .collect()
48    };
49
50    eprintln!("Requesting pull requests ...");
51    let pull_requests = host.merged_pull_requests(&config.repository).await?;
52
53    eprintln!("Filtering pull requests ...");
54    let pull_requests: Vec<GitPullRequest> = pull_requests
55        .into_iter()
56        .filter(|pull_request| {
57            let Ok(base_obj) = repository.revparse_single(&pull_request.base_sha) else {
58                return false;
59            };
60            let Ok(merge_obj) = repository.revparse_single(&pull_request.merge_sha) else {
61                return false;
62            };
63
64            range_commit_ids.contains(&base_obj.id()) || range_commit_ids.contains(&merge_obj.id())
65        })
66        .collect();
67
68    if !config.dry_run && std::env::current_dir()? != config.directory {
69        eprintln!("Entering repository directory ...");
70        std::env::set_current_dir(&config.directory)?;
71    }
72
73    eprintln!("Entering repository directory ...");
74
75    for pull_request in pull_requests {
76        let program = "git";
77        let mut command = Command::new(program);
78        let mut args = vec!["bisect".to_owned(), "skip".to_owned()];
79
80        args.push(format!(
81            "{start}..{end}^",
82            start = pull_request.base_sha,
83            end = pull_request.merge_sha,
84        ));
85
86        let formatted_args = args.join(" ");
87
88        command.args(args);
89
90        println!(
91            "# Pull request #{number}: {title:?}",
92            number = pull_request.identifier,
93            title = pull_request.title
94        );
95
96        if config.dry_run {
97            command.spawn().and_then(|mut child| child.wait())?;
98        } else {
99            println!("{program} {formatted_args}");
100        }
101    }
102
103    Ok(ExitStatus::from_raw(0))
104}