Fix Push level native repository setup

- Materialize the Push level with a real local/remote Git fixture that shares the first two commits, adds a remote-only fourth commit, and leaves a local third commit to rebase.
- Strengthen Push validation to require the rebased/pushed state and all expected files instead of only checking that a push command happened.
- Keep the level solution tests exercising the same native sandbox shape users see in the app.
This commit is contained in:
Joe Tretter
2026-05-06 22:40:06 -05:00
parent 7bb14216ef
commit 8e88b5e7c5
3 changed files with 70 additions and 6 deletions

View File

@@ -60,7 +60,7 @@ class GitRepositoryRuntime private constructor(
runGit(nativeGit, sandbox, listOf("checkout", "-B", desired.headBranch))
}
materializeNativeGitState(nativeGit, sandbox, desired)
materializeNativeGitState(nativeGit, sandbox, desired, level.id)
}
return inspectSandbox(level)
@@ -385,7 +385,12 @@ class GitRepositoryRuntime private constructor(
return body
}
private fun materializeNativeGitState(nativeGit: File, sandbox: File, desired: RepoState) {
private fun materializeNativeGitState(nativeGit: File, sandbox: File, desired: RepoState, levelId: String) {
if (levelId == "push") {
materializeNativePushLevel(nativeGit, sandbox)
return
}
desired.config.forEach { (key, value) ->
runGit(nativeGit, sandbox, listOf("config", key, value))
}
@@ -437,6 +442,42 @@ class GitRepositoryRuntime private constructor(
}
}
private fun materializeNativePushLevel(nativeGit: File, sandbox: File) {
sandbox.listFiles()
?.filterNot { it.name == ".git" }
?.forEach { it.deleteRecursively() }
fun writeFile(name: String, content: String = "$name\n") {
File(sandbox, name).apply {
parentFile?.mkdirs()
writeText(content)
}
}
fun commitIn(directory: File, message: String, vararg paths: String) {
runGit(nativeGit, directory, listOf("add") + paths)
runGit(nativeGit, directory, listOf("commit", "-m", message))
}
writeFile("file1")
commitIn(sandbox, "First commit", "file1")
writeFile("file2")
commitIn(sandbox, "Second commit", "file2")
val remoteWorkTree = File(sandbox.parentFile ?: sandbox, "${sandbox.name}-origin")
remoteWorkTree.deleteRecursively()
runGit(nativeGit, sandbox.parentFile ?: sandbox, listOf("clone", sandbox.absolutePath, remoteWorkTree.absolutePath))
runGit(nativeGit, remoteWorkTree, listOf("config", "receive.denyCurrentBranch", "ignore"))
File(remoteWorkTree, "file4").writeText("file4\n")
commitIn(remoteWorkTree, "Fourth commit", "file4")
writeFile("file3")
commitIn(sandbox, "Third commit", "file3")
runGit(nativeGit, sandbox, listOf("remote", "add", "origin", File(remoteWorkTree, ".git").absolutePath))
runGit(nativeGit, sandbox, listOf("fetch", "origin"))
runGit(nativeGit, sandbox, listOf("branch", "--set-upstream-to=origin/master", "master"))
}
private fun filesForSetupCommit(files: List<GitFile>, index: Int, commitCount: Int): List<GitFile> {
if (files.isEmpty()) return emptyList()
if (commitCount <= 1) return files

View File

@@ -13,8 +13,31 @@ internal fun pushLevel(): Level = level(
description = "Your local master branch has diverged from the remote origin/master branch. Rebase your branch onto origin/master and push it to remote.",
hints = listOf("Take a look at `git fetch`, `git pull`, and `git push`."),
commandSuggestions = listOf("git pull --rebase origin master", "git push origin master"),
setup = { RepoState(initialized = true, remotes = mapOf("origin" to "remote"), branches = mapOf("master" to 3), fetchedBranches = setOf("origin/master")) },
validator = repoPredicate { repo -> "origin/master" in repo.pushedBranches },
setup = {
RepoState(
initialized = true,
files = listOf(
GitFile("file1", tracked = true),
GitFile("file2", tracked = true),
GitFile("file3", tracked = true),
),
commits = listOf(
CommitNode("0000001", "First commit"),
CommitNode("0000002", "Second commit"),
CommitNode("0000003", "Third commit"),
),
remotes = mapOf("origin" to "remote"),
branches = mapOf("master" to 3),
fetchedBranches = setOf("origin/master"),
)
},
validator = repoPredicate { repo ->
"origin/master" in repo.pushedBranches &&
repo.commits.size >= 4 &&
listOf("file1", "file2", "file3", "file4").all { expected ->
repo.files.any { file -> file.name == expected && file.tracked && !file.deleted }
}
},
testCases = listOf(
levelTestCase("pull rebase then push", "git pull --rebase origin master", "git push origin master"),
levelTestCase("fetch rebase push", "git fetch origin", "git rebase origin/master", "git push origin master"),