diff --git a/README.md b/README.md index 8eba404..d2fe7fc 100644 --- a/README.md +++ b/README.md @@ -130,10 +130,11 @@ This will show you which flags can safely enabled, and which flags require a mig ### --bisect `--bisect` flag allows you to bisect Bazel versions to find which version introduced a build failure. You can specify the range of versions to bisect with `--bisect=..`, where GOOD is the last known working Bazel version and BAD is the first known non-working Bazel version. Bazelisk uses [GitHub's compare API](https://docs.github.com/en/rest/commits/commits#compare-two-commits) to get the list of commits to bisect. When GOOD is not an ancestor of BAD, GOOD is reset to their merge base commit. +The meaning of GOOD and BAD can be reversed by prefixing the range with `~`, e.g. `--bisect=!6.0.0..HEAD` will find the first version 6.0.0 and HEAD that *fixes* the build. Examples: ```shell -# Bisect between 6.0.0 and Bazel at HEAD +# Bisect between 6.0.0 and Bazel at HEAD to find the first commit that breaks the build. bazelisk --bisect=6.0.0..HEAD test //foo:bar_test # Bisect between 6.1.0 and the second release candidate of Bazel 6.2.0 @@ -141,6 +142,9 @@ bazelisk --bisect=6.1.0..release-6.2.0rc2 test //foo:bar_test # Bisect between two commits on the main branch (or branches with `release-` prefix) of the Bazel GitHub repository. bazelisk --bisect=.. test //foo:bar_test + +# Bisect between 6.0.0 and Bazel at HEAD to find the first commit that *fixes* the build. +bazelisk --bisect=~6.0.0..HEAD test //foo:bar_test ``` Note that, Bazelisk uses prebuilt Bazel binaries at commits on the main and release branches, therefore you cannot bisect your local commits. @@ -227,8 +231,6 @@ For more information, you may read about the [`GOPATH` environment variable](htt - Add support for checked-in Bazel binaries. - When the version label is set to a commit hash, first download a matching binary version of Bazel, then build Bazel automatically at that commit and use the resulting binary. -- Add support to automatically bisect a build failure to a culprit commit in Bazel. - If you notice that you could successfully build your project using version X, but not using version X+1, then Bazelisk should be able to figure out the commit that caused the breakage and the Bazel team can easily fix the problem. ## FAQ diff --git a/core/core.go b/core/core.go index 7a8cf22..620abe4 100644 --- a/core/core.go +++ b/core/core.go @@ -165,14 +165,14 @@ func RunBazeliskWithArgsFuncAndConfigAndOut(argsFunc ArgsFunc, repos *Repositori } else if len(args) > 0 && strings.HasPrefix(args[0], "--bisect") { // When --bisect is present, we run the bisect logic. if !strings.HasPrefix(args[0], "--bisect=") { - return -1, fmt.Errorf("Error: --bisect must have a value. Expected format: '--bisect=..'") + return -1, fmt.Errorf("Error: --bisect must have a value. Expected format: '--bisect=[!]..'") } value := args[0][len("--bisect="):] commits := strings.Split(value, "..") if len(commits) == 2 { bisect(commits[0], commits[1], args[1:], bazeliskHome, repos, config) } else { - return -1, fmt.Errorf("Error: Invalid format for --bisect. Expected format: '--bisect=..'") + return -1, fmt.Errorf("Error: Invalid format for --bisect. Expected format: '--bisect=[!]..'") } } @@ -214,11 +214,11 @@ func getBazelCommand(args []string) (string, error) { // getBazeliskHome returns the path to the Bazelisk home directory. func getBazeliskHome(config config.Config) (string, error) { - bazeliskHome := config.Get("BAZELISK_HOME_" + strings.ToUpper(runtime.GOOS)) + bazeliskHome := config.Get("BAZELISK_HOME_" + strings.ToUpper(runtime.GOOS)) if len(bazeliskHome) == 0 { bazeliskHome = config.Get("BAZELISK_HOME") } - + if len(bazeliskHome) == 0 { userCacheDir, err := os.UserCacheDir() if err != nil { @@ -766,47 +766,47 @@ func sendRequest(url string, config config.Config) (*http.Response, error) { return client.Do(req) } -func getBazelCommitsBetween(goodCommit string, badCommit string, config config.Config) (string, []string, error) { +func getBazelCommitsBetween(oldCommit string, newCommit string, config config.Config) (string, []string, error) { commitList := make([]string, 0) page := 1 perPage := 250 // 250 is the maximum number of commits per page for { - url := fmt.Sprintf("https://api.github.com/repos/bazelbuild/bazel/compare/%s...%s?page=%d&per_page=%d", goodCommit, badCommit, page, perPage) + url := fmt.Sprintf("https://api.github.com/repos/bazelbuild/bazel/compare/%s...%s?page=%d&per_page=%d", oldCommit, newCommit, page, perPage) response, err := sendRequest(url, config) if err != nil { - return goodCommit, nil, fmt.Errorf("Error fetching commit data: %v", err) + return oldCommit, nil, fmt.Errorf("Error fetching commit data: %v", err) } defer response.Body.Close() body, err := io.ReadAll(response.Body) if err != nil { - return goodCommit, nil, fmt.Errorf("Error reading response body: %v", err) + return oldCommit, nil, fmt.Errorf("Error reading response body: %v", err) } if response.StatusCode == http.StatusNotFound { - return goodCommit, nil, fmt.Errorf("repository or commit not found: %s", string(body)) + return oldCommit, nil, fmt.Errorf("repository or commit not found: %s", string(body)) } else if response.StatusCode == 403 { - return goodCommit, nil, fmt.Errorf("github API rate limit hit, consider setting BAZELISK_GITHUB_TOKEN: %s", string(body)) + return oldCommit, nil, fmt.Errorf("github API rate limit hit, consider setting BAZELISK_GITHUB_TOKEN: %s", string(body)) } else if response.StatusCode != http.StatusOK { - return goodCommit, nil, fmt.Errorf("unexpected response status code %d: %s", response.StatusCode, string(body)) + return oldCommit, nil, fmt.Errorf("unexpected response status code %d: %s", response.StatusCode, string(body)) } var compResp compareResponse err = json.Unmarshal(body, &compResp) if err != nil { - return goodCommit, nil, fmt.Errorf("Error unmarshaling JSON: %v", err) + return oldCommit, nil, fmt.Errorf("Error unmarshaling JSON: %v", err) } - + if len(compResp.Commits) == 0 { break } mergeBaseCommit := compResp.MergeBaseCommit.SHA if mergeBaseCommit != compResp.BaseCommit.SHA { - fmt.Printf("The good Bazel commit is not an ancestor of the bad Bazel commit, overriding the good Bazel commit to the merge base commit %s\n", mergeBaseCommit) - goodCommit = mergeBaseCommit + fmt.Printf("The old Bazel commit is not an ancestor of the good Bazel commit, overriding the old Bazel commit to the merge base commit %s\n", mergeBaseCommit) + oldCommit = mergeBaseCommit } for _, commit := range compResp.Commits { @@ -825,32 +825,38 @@ func getBazelCommitsBetween(goodCommit string, badCommit string, config config.C } if len(commitList) == 0 { - return goodCommit, nil, fmt.Errorf("no commits found between (%s, %s], the good commit should be first, maybe try with --bisect=%s..%s ?", goodCommit, badCommit, badCommit, goodCommit) + return oldCommit, nil, fmt.Errorf("no commits found between (%s, %s], the old commit should be first, maybe try with --bisect=%s..%s or --bisect=~%s..%s?", oldCommit, newCommit, newCommit, oldCommit, oldCommit, newCommit) } - fmt.Printf("Found %d commits between (%s, %s]\n", len(commitList), goodCommit, badCommit) - return goodCommit, commitList, nil + fmt.Printf("Found %d commits between (%s, %s]\n", len(commitList), oldCommit, newCommit) + return oldCommit, commitList, nil } -func bisect(goodCommit string, badCommit string, args []string, bazeliskHome string, repos *Repositories, config config.Config) { +func bisect(oldCommit string, newCommit string, args []string, bazeliskHome string, repos *Repositories, config config.Config) { + var oldCommitIs string + if strings.HasPrefix(oldCommit, "~") { + oldCommit = oldCommit[1:] + oldCommitIs = "bad" + } else { + oldCommitIs = "good" + } - // 1. Get the list of commits between goodCommit and badCommit - fmt.Printf("\n\n--- Getting the list of commits between %s and %s\n\n", goodCommit, badCommit) - goodCommit, commitList, err := getBazelCommitsBetween(goodCommit, badCommit, config) + // 1. Get the list of commits between oldCommit and newCommit + fmt.Printf("\n\n--- Getting the list of commits between %s and %s\n\n", oldCommit, newCommit) + oldCommit, commitList, err := getBazelCommitsBetween(oldCommit, newCommit, config) if err != nil { log.Fatalf("Failed to get commits: %v", err) - os.Exit(1) } - // 2. Check if goodCommit is actually good - fmt.Printf("\n\n--- Verifying if the given good Bazel commit (%s) is actually good\n\n", goodCommit) - bazelExitCode, err := testWithBazelAtCommit(goodCommit, args, bazeliskHome, repos, config) + // 2. Check if oldCommit is actually good/bad as specified + fmt.Printf("\n\n--- Verifying if the given %s Bazel commit (%s) is actually %s\n\n", oldCommitIs, oldCommit, oldCommitIs) + bazelExitCode, err := testWithBazelAtCommit(oldCommit, args, bazeliskHome, repos, config) if err != nil { log.Fatalf("could not run Bazel: %v", err) - os.Exit(1) } - if bazelExitCode != 0 { + if oldCommitIs == "good" && bazelExitCode != 0 { fmt.Printf("Failure: Given good bazel commit is already broken.\n") - os.Exit(1) + } else if oldCommitIs == "bad" && bazelExitCode == 0 { + fmt.Printf("Failure: Given bad bazel commit is already fixed.\n") } // 3. Bisect commits @@ -864,24 +870,39 @@ func bisect(goodCommit string, badCommit string, args []string, bazeliskHome str bazelExitCode, err := testWithBazelAtCommit(midCommit, args, bazeliskHome, repos, config) if err != nil { log.Fatalf("could not run Bazel: %v", err) - os.Exit(1) } if bazelExitCode == 0 { fmt.Printf("\n\n--- Succeeded at %s\n\n", midCommit) - left = mid + 1 + if oldCommitIs == "good" { + left = mid + 1 + } else { + right = mid + } } else { fmt.Printf("\n\n--- Failed at %s\n\n", midCommit) - right = mid + if oldCommitIs == "good" { + right = mid + } else { + left = mid + 1 + } } } // 4. Print the result fmt.Printf("\n\n--- Bisect Result\n\n") if right == len(commitList) { - fmt.Printf("first bad commit not found, every commit succeeded.\n") + if oldCommitIs == "good" { + fmt.Printf("first bad commit not found, every commit succeeded.\n") + } else { + fmt.Printf("first good commit not found, every commit failed.\n") + } } else { - firstBadCommit := commitList[right] - fmt.Printf("first bad commit is https://github.com/bazelbuild/bazel/commit/%s\n", firstBadCommit) + flippingCommit := commitList[right] + if oldCommitIs == "good" { + fmt.Printf("first bad commit is https://github.com/bazelbuild/bazel/commit/%s\n", flippingCommit) + } else { + fmt.Printf("first good commit is https://github.com/bazelbuild/bazel/commit/%s\n", flippingCommit) + } } os.Exit(0)