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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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 |
|
| 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. |
|
| `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. |
|
| `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. |
|
| `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"
|
applicationId = "solutions.tretter.githugandroid"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 152
|
versionCode = 153
|
||||||
versionName = "0.1.151"
|
versionName = "0.1.152"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|||||||
@@ -584,8 +584,8 @@ class GitRepositoryRuntime private constructor(
|
|||||||
|
|
||||||
private fun materializeNativeGitState(nativeGit: File, sandbox: File, desired: RepoState, level: Level) {
|
private fun materializeNativeGitState(nativeGit: File, sandbox: File, desired: RepoState, level: Level) {
|
||||||
level.nativeSetup?.let { setup ->
|
level.nativeSetup?.let { setup ->
|
||||||
val nativeSetup = NativeLevelSetup(sandbox) { directory, arguments ->
|
val nativeSetup = NativeLevelSetup(sandbox) { directory, arguments, environment ->
|
||||||
runGit(nativeGit, directory, arguments).exitCode
|
runGit(nativeGit, directory, arguments, environment).exitCode
|
||||||
}
|
}
|
||||||
if (nativeSetup.setup()) {
|
if (nativeSetup.setup()) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import java.io.File
|
|||||||
|
|
||||||
class NativeLevelSetup internal constructor(
|
class NativeLevelSetup internal constructor(
|
||||||
internal val sandbox: File,
|
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() {
|
fun resetFiles() {
|
||||||
sandbox.listFiles()
|
sandbox.listFiles()
|
||||||
?.filterNot { it.name == ".git" }
|
?.filterNot { it.name == ".git" }
|
||||||
@@ -13,9 +15,9 @@ class NativeLevelSetup internal constructor(
|
|||||||
git("checkout", "-B", "master")
|
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) {
|
fun initRepo(directory: File) {
|
||||||
directory.mkdirs()
|
directory.mkdirs()
|
||||||
@@ -43,10 +45,15 @@ class NativeLevelSetup internal constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun commit(message: String, author: String? = null) {
|
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) {
|
if (author == null) {
|
||||||
git("commit", "-m", message)
|
gitWithEnvironment(environment, "commit", "-m", message)
|
||||||
} else {
|
} 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) {
|
fun tag(name: String) {
|
||||||
git("tag", "-f", name)
|
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(
|
internal fun bisectLevel(): Level = level(
|
||||||
id = "bisect",
|
id = "bisect",
|
||||||
title = "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(
|
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.",
|
"`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(
|
commandSuggestions = listOf(
|
||||||
"./test-balance.sh",
|
"./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 bisect run ./test-balance.sh",
|
||||||
|
"git rev-parse --short HEAD^",
|
||||||
),
|
),
|
||||||
setup = {
|
setup = {
|
||||||
RepoState(
|
RepoState(
|
||||||
@@ -68,22 +71,26 @@ internal fun bisectLevel(): Level = level(
|
|||||||
addCommit("Update help text", "README")
|
addCommit("Update help text", "README")
|
||||||
append("notes.txt", "Forecast still depends on the balance check\n")
|
append("notes.txt", "Forecast still depends on the balance check\n")
|
||||||
addCommit("Add forecast note", "notes.txt")
|
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")
|
addCommit("Polish report labels", "README")
|
||||||
true
|
true
|
||||||
},
|
},
|
||||||
validator = { repo, command ->
|
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 normalized = command.trim()
|
||||||
val answerMatches = badCommit != null &&
|
lastGoodCommit != null &&
|
||||||
normalized.isNotBlank() &&
|
normalized.length >= 4 &&
|
||||||
!normalized.startsWith("git ") &&
|
!normalized.startsWith("git ") &&
|
||||||
(normalized.startsWith(badCommit.id, ignoreCase = true) || badCommit.id.startsWith(normalized, ignoreCase = true))
|
(normalized.startsWith(lastGoodCommit.id, ignoreCase = true) || lastGoodCommit.id.startsWith(normalized, ignoreCase = true))
|
||||||
val bisectFoundBadCommit = normalized.startsWith("git bisect run", ignoreCase = true) &&
|
|
||||||
"bisect-found" in repo.maintenanceActions
|
|
||||||
answerMatches || bisectFoundBadCommit
|
|
||||||
},
|
},
|
||||||
testCases = listOf(
|
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.",
|
"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\""),
|
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 {
|
private companion object {
|
||||||
|
|||||||
Reference in New Issue
Block a user