Skip to main content

git_assist/command/bisect/
mod.rs

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