Ask for last good commit in bisect level
This commit is contained in:
@@ -119,7 +119,7 @@ This file records the upstream Ruby setup and validation intent beside the Andro
|
||||
| `squash` | Commit count is two. | Commit count at most two and "Adding README" remains. | Slightly stricter on preserving the base README commit. |
|
||||
| `merge_squash` | Commit count is three and all long-feature changes are included. | Squash action recorded and a commit exists. | Known gap: Android does not yet verify all squash file/content effects. |
|
||||
| `reorder` | `git log` subject order matches `Third.*Second.*First.*Initial`. | Modeled commit order becomes First, Second, Third. | Equivalent relative to Android's oldest-first commit list. |
|
||||
| `bisect` | Answer is hash prefix `18ed2ac` after using Ruby/make fixture. | Learner can run `git bisect start HEAD known-good` and `git bisect run ./test-balance.sh`; Android accepts the discovered bad hash or the final bisect state. | Same lesson, different fixture to keep it playable without Ruby/make. |
|
||||
| `bisect` | Answer is hash prefix `18ed2ac` after using Ruby/make fixture. | Learner can run `git bisect start`, mark `HEAD` bad and `known-good` good, run `git bisect run ./test-balance.sh`, then answer the last good commit hash. | Same bisect lesson, different fixture and final answer target to keep the Android flow clear without Ruby/make. |
|
||||
| `stage_lines` | Staged diff contains first feature line; unstaged diff contains second feature line. | `feature.rb` is staged. | Known gap: Android does not yet model partial hunk staging. |
|
||||
| `find_old_branch` | Current branch is `solve_world_hunger`. | Same. | Equivalent. |
|
||||
| `revert` | More than three commits and a revert of "Bad commit" exists. | A commit message starts with `Revert`. | Slightly looser; sufficient for current fixture. |
|
||||
|
||||
@@ -134,7 +134,7 @@ The Android port keeps the upstream GitHug level order, but some upstream fixtur
|
||||
|
||||
| Level | Upstream behavior | Android behavior | Why it differs |
|
||||
| --- | --- | --- | --- |
|
||||
| `bisect` | Copies the upstream Ruby fixture. The learner tests each checked-out commit with `ruby prog.rb 5` or `make test`, then answers the abbreviated hash `18ed2ac`. | Creates a native Git history with `balance.txt` and `test-balance.sh`. The learner can run `./test-balance.sh` or `sh test-balance.sh`, then use `git bisect start HEAD known-good` and `git bisect run ./test-balance.sh`. The level accepts the discovered bad commit hash or the final bisect state. | Android does not bundle Ruby or `make`. The replacement still demonstrates the real `git bisect` workflow: identify known good/bad endpoints, run a test at each checked-out commit, and find the first bad commit. |
|
||||
| `bisect` | Copies the upstream Ruby fixture. The learner tests each checked-out commit with `ruby prog.rb 5` or `make test`, then answers the abbreviated hash `18ed2ac`. | Creates a native Git history with `balance.txt` and `test-balance.sh`. The learner can run `./test-balance.sh` or `sh test-balance.sh`, mark `HEAD` bad and `known-good` good, then use `git bisect run ./test-balance.sh`. The level accepts only the abbreviated hash of the last good commit before the break. | Android does not bundle Ruby or `make`. The replacement still demonstrates the real `git bisect` workflow: identify known good/bad endpoints, run a test at each checked-out commit, and use the discovered first bad commit to identify the last good commit. |
|
||||
| `clone` / `clone_to_folder` | Clones `https://github.com/Gazler/cloneme` and checks the cloned repository content. | Accepts the intended clone command and models the resulting folder. | The app must remain playable offline and avoid relying on GitHub network access from a phone. |
|
||||
| `pull`, `fetch`, `push`, `push_branch`, `push_tags` | Use remote-style workflows from upstream fixtures. | Use local synthetic remotes created inside the sandbox and validate fetched/pushed refs through `RepoState`. | This preserves Git behavior without external network dependencies. |
|
||||
| `contribute` | Expects cloning upstream and finding a commit authored by the configured user. | Treated as a mobile/offline final prompt with a nonblank response. | The original workflow leaves the sandbox and depends on external contribution infrastructure. |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user