Skip to content

Commit

Permalink
Let --bisect=~<old>..<new> find the first fixing commit (#613)
Browse files Browse the repository at this point in the history
* Let `--bisect=~<old>..<new>` find the first fixing commit

* Address comments
  • Loading branch information
fmeum authored Sep 19, 2024
1 parent c6756d0 commit bb3bb3f
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 35 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,17 +138,21 @@ 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=<GOOD>..<BAD>`, 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
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=<good commit hash>..<bad commit hash> 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.
Expand Down Expand Up @@ -235,8 +239,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

Expand Down
85 changes: 53 additions & 32 deletions core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,14 +166,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=<good bazel commit>..<bad bazel commit>'")
return -1, fmt.Errorf("Error: --bisect must have a value. Expected format: '--bisect=[~]<good bazel commit>..<bad bazel commit>'")
}
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=<good bazel commit>..<bad bazel commit>'")
return -1, fmt.Errorf("Error: Invalid format for --bisect. Expected format: '--bisect=[~]<good bazel commit>..<bad bazel commit>'")
}
}

Expand Down Expand Up @@ -771,37 +771,37 @@ 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 {
Expand All @@ -810,8 +810,8 @@ func getBazelCommitsBetween(goodCommit string, badCommit string, config config.C

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 new Bazel commit, overriding the old Bazel commit to the merge base commit %s\n", mergeBaseCommit)
oldCommit = mergeBaseCommit
}

for _, commit := range compResp.Commits {
Expand All @@ -830,32 +830,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
Expand All @@ -869,24 +875,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)
Expand Down

0 comments on commit bb3bb3f

Please sign in to comment.