Ask for last good commit in bisect level

This commit is contained in:
Joe Tretter
2026-05-18 20:19:34 -05:00
parent 008a322134
commit 1ce4df1209
7 changed files with 62 additions and 24 deletions

View File

@@ -19,8 +19,8 @@ android {
applicationId = "solutions.tretter.githugandroid"
minSdk = 26
targetSdk = 35
versionCode = 152
versionName = "0.1.151"
versionCode = 153
versionName = "0.1.152"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true

View File

@@ -584,8 +584,8 @@ class GitRepositoryRuntime private constructor(
private fun materializeNativeGitState(nativeGit: File, sandbox: File, desired: RepoState, level: Level) {
level.nativeSetup?.let { setup ->
val nativeSetup = NativeLevelSetup(sandbox) { directory, arguments ->
runGit(nativeGit, directory, arguments).exitCode
val nativeSetup = NativeLevelSetup(sandbox) { directory, arguments, environment ->
runGit(nativeGit, directory, arguments, environment).exitCode
}
if (nativeSetup.setup()) {
return

View File

@@ -4,8 +4,10 @@ import java.io.File
class NativeLevelSetup internal constructor(
internal val sandbox: File,
private val runGit: (File, List<String>) -> Int,
private val runGit: (File, List<String>, Map<String, String>) -> Int,
) {
private var commitSequence = 0
fun resetFiles() {
sandbox.listFiles()
?.filterNot { it.name == ".git" }
@@ -13,9 +15,9 @@ class NativeLevelSetup internal constructor(
git("checkout", "-B", "master")
}
fun git(vararg arguments: String): Int = runGit(sandbox, arguments.toList())
fun git(vararg arguments: String): Int = runGit(sandbox, arguments.toList(), emptyMap())
fun git(directory: File, vararg arguments: String): Int = runGit(directory, arguments.toList())
fun git(directory: File, vararg arguments: String): Int = runGit(directory, arguments.toList(), emptyMap())
fun initRepo(directory: File) {
directory.mkdirs()
@@ -43,10 +45,15 @@ class NativeLevelSetup internal constructor(
}
fun commit(message: String, author: String? = null) {
val commitDate = nextDeterministicCommitDate()
val environment = mapOf(
"GIT_AUTHOR_DATE" to commitDate,
"GIT_COMMITTER_DATE" to commitDate,
)
if (author == null) {
git("commit", "-m", message)
gitWithEnvironment(environment, "commit", "-m", message)
} else {
git("commit", "--author", author, "-m", message)
gitWithEnvironment(environment, "commit", "--author", author, "-m", message)
}
}
@@ -85,4 +92,15 @@ class NativeLevelSetup internal constructor(
fun tag(name: String) {
git("tag", "-f", name)
}
private fun gitWithEnvironment(environment: Map<String, String>, vararg arguments: String): Int {
return runGit(sandbox, arguments.toList(), environment)
}
private fun nextDeterministicCommitDate(): String {
commitSequence += 1
val minute = (commitSequence / 60).toString().padStart(2, '0')
val second = (commitSequence % 60).toString().padStart(2, '0')
return "2000-01-01T00:$minute:$second+0000"
}
}

View File

@@ -10,16 +10,19 @@ package solutions.tretter.githugandroid
internal fun bisectLevel(): Level = level(
id = "bisect",
title = "Bisect",
description = "A balance check started failing somewhere in the history. Run `./test-balance.sh` to test the current commit. Use `git bisect` to identify the first bad commit.",
description = "A balance check started failing somewhere in the history. The current HEAD is bad: `./test-balance.sh` prints `balance broken`. Use `git bisect` to locate the break, then enter the abbreviated hash of the last good commit.",
hints = listOf(
"Start with a known bad commit and a known good commit.",
"Mark the current HEAD as bad, because the test fails there.",
"`known-good` marks a commit where the balance check passes.",
"You can automate the search with `git bisect run ./test-balance.sh`.",
"`git bisect run ./test-balance.sh` identifies the first bad commit; the last good commit is its parent in this linear history.",
),
commandSuggestions = listOf(
"./test-balance.sh",
"git bisect start HEAD known-good",
"git bisect start",
"git bisect bad HEAD",
"git bisect good known-good",
"git bisect run ./test-balance.sh",
"git rev-parse --short HEAD^",
),
setup = {
RepoState(
@@ -68,22 +71,26 @@ internal fun bisectLevel(): Level = level(
addCommit("Update help text", "README")
append("notes.txt", "Forecast still depends on the balance check\n")
addCommit("Add forecast note", "notes.txt")
append("README", "Use git bisect to find the first broken commit.\n")
append("README", "Use git bisect to find the last good commit before the report broke.\n")
addCommit("Polish report labels", "README")
true
},
validator = { repo, command ->
val badCommit = repo.commits.firstOrNull { it.message == "Break closing balance calculation" }
val lastGoodCommit = repo.commits.firstOrNull { it.message == "Add audit note" }
val normalized = command.trim()
val answerMatches = badCommit != null &&
normalized.isNotBlank() &&
lastGoodCommit != null &&
normalized.length >= 4 &&
!normalized.startsWith("git ") &&
(normalized.startsWith(badCommit.id, ignoreCase = true) || badCommit.id.startsWith(normalized, ignoreCase = true))
val bisectFoundBadCommit = normalized.startsWith("git bisect run", ignoreCase = true) &&
"bisect-found" in repo.maintenanceActions
answerMatches || bisectFoundBadCommit
(normalized.startsWith(lastGoodCommit.id, ignoreCase = true) || lastGoodCommit.id.startsWith(normalized, ignoreCase = true))
},
testCases = listOf(
levelTestCase("automated bisect", "git bisect start HEAD known-good", "git bisect run ./test-balance.sh"),
levelTestCase(
"answer last good commit",
"git bisect start",
"git bisect bad HEAD",
"git bisect good known-good",
"git bisect run ./test-balance.sh",
"c8c7c00",
),
),
)

View File

@@ -139,6 +139,19 @@ class LevelSolutionsTest {
"A normal commit using the current system date must not solve the commit_in_future level.",
futureCommitExercise.validator(currentDateCommitRepo, "git commit -m \"Current date commit\""),
)
val bisectExercise = bisectLevel()
val bisectRuntime = GitRepositoryRuntime(runtimeRoot, gitBinary)
var bisectRepo = bisectRuntime.prepareLevel(bisectExercise)
listOf("git bisect start", "git bisect bad HEAD", "git bisect good known-good").forEach { command ->
val (nextRepo, _) = bisectRuntime.execute(bisectExercise, bisectRepo, command)
bisectRepo = nextRepo
}
val (bisectRunRepo, _) = bisectRuntime.execute(bisectExercise, bisectRepo, "git bisect run ./test-balance.sh")
assertFalse(
"Running bisect should not solve the bisect level until the learner enters the last good commit hash.",
bisectExercise.validator(bisectRunRepo, "git bisect run ./test-balance.sh"),
)
}
private companion object {