Files
Githug-Android/app/src/main/java/solutions/tretter/githugandroid/NativeLevelFixtures.kt
Joe Tretter 91e2df3ffe Improve upstream parity for level fixtures
- Add native per-level fixtures for complex histories, tags, branch graphs, rebases, resets, restores, stashes, and local remotes.
- Strengthen repository inspection so branch commit counts, remote-tracking branches, and detached tag checkouts reflect real Git state.
- Align Fetch, Pull, Push Branch, Branch At, and Rebase validators/tests with upstream-style repository state.
- Keep the prompt callout and native filesystem tab-completion fixes from the previous batch.
2026-05-07 09:21:04 -05:00

390 lines
12 KiB
Kotlin

package solutions.tretter.githugandroid
import java.io.File
internal fun materializeNativeLevelFixture(
levelId: String,
sandbox: File,
runGit: (File, List<String>) -> Int,
): Boolean {
val fixture = NativeLevelFixture(sandbox, runGit)
return when (levelId) {
"branch_at" -> fixture.branchAt()
"checkout_tag" -> fixture.checkoutTag()
"checkout_tag_over_branch" -> fixture.checkoutTagOverBranch()
"fetch" -> fixture.fetch()
"pull" -> fixture.pull()
"push_branch" -> fixture.pushBranch()
"push_tags" -> fixture.pushTags()
"merge" -> fixture.merge()
"rebase" -> fixture.rebase()
"rebase_onto" -> fixture.rebaseOnto()
"merge_squash" -> fixture.mergeSquash()
"reset" -> fixture.reset()
"reset_soft" -> fixture.resetSoft()
"restore" -> fixture.restore()
"squash" -> fixture.squash()
"reorder" -> fixture.reorder()
"rename_commit" -> fixture.renameCommit()
"revert" -> fixture.revert()
"stash" -> fixture.stash()
"checkout_file" -> fixture.checkoutFile()
else -> false
}
}
private class NativeLevelFixture(
private val sandbox: File,
private val runGit: (File, List<String>) -> Int,
) {
private fun resetFiles() {
sandbox.listFiles()
?.filterNot { it.name == ".git" }
?.forEach { it.deleteRecursively() }
git("checkout", "-B", "master")
}
private fun git(vararg arguments: String): Int = runGit(sandbox, arguments.toList())
private fun git(directory: File, vararg arguments: String): Int = runGit(directory, arguments.toList())
private fun initRepo(directory: File) {
directory.mkdirs()
val initResult = git(directory, "init", "-b", "master")
if (initResult != 0) {
git(directory, "init")
git(directory, "checkout", "-B", "master")
}
git(directory, "config", "receive.denyCurrentBranch", "ignore")
}
private fun write(path: String, content: String = "") {
File(sandbox, path).apply {
parentFile?.mkdirs()
writeText(content)
}
}
private fun append(path: String, content: String) {
File(sandbox, path).appendText(content)
}
private fun add(vararg paths: String) {
git("add", *paths)
}
private fun commit(message: String) {
git("commit", "-m", message)
}
private fun addCommit(message: String, vararg paths: String) {
add(*paths)
commit(message)
}
private fun writeIn(directory: File, path: String, content: String = "") {
File(directory, path).apply {
parentFile?.mkdirs()
writeText(content)
}
}
private fun addCommitIn(directory: File, message: String, vararg paths: String) {
git(directory, "add", *paths)
git(directory, "commit", "-m", message)
}
private fun siblingRepo(name: String): File {
val directory = File(sandbox.parentFile ?: sandbox, "${sandbox.name}-$name")
directory.deleteRecursively()
initRepo(directory)
return directory
}
private fun checkoutNew(branch: String) {
git("checkout", "-b", branch)
}
private fun checkout(branch: String) {
git("checkout", branch)
}
private fun tag(name: String) {
git("tag", "-f", name)
}
fun branchAt(): Boolean {
resetFiles()
write("file1")
addCommit("Adding file1", "file1")
write("file1", "content")
addCommit("Updating file1", "file1")
append("file1", "\nAdding some more text")
addCommit("Updating file1 again", "file1")
return true
}
fun checkoutFile(): Boolean {
resetFiles()
write("config.rb", "This is the initial config file")
addCommit("Added initial config file", "config.rb")
append("config.rb", "\nThese are changes you don't want to keep!")
return true
}
fun checkoutTag(): Boolean {
resetFiles()
write("app.rb")
addCommit("Initial commit", "app.rb")
append("app.rb", "some changes\n")
addCommit("Some changes", "app.rb")
tag("v1.0")
append("app.rb", "some more changes\n")
addCommit("Some more changes", "app.rb")
tag("v1.2")
append("app.rb", "yet more changes\n")
addCommit("Yet more changes", "app.rb")
append("app.rb", "changes galore\n")
addCommit("Changes galore", "app.rb")
tag("v1.5")
return true
}
fun checkoutTagOverBranch(): Boolean {
checkoutTag()
checkoutNew("v1.2")
write("file3", "some feature\n")
addCommit("Developing new features", "file3")
checkout("master")
return true
}
fun fetch(): Boolean {
resetFiles()
write("master_file")
addCommit("Commits master_file", "master_file")
val remote = siblingRepo("origin")
git("remote", "add", "fetch-setup-origin", File(remote, ".git").absolutePath)
git("push", "fetch-setup-origin", "master")
git("remote", "remove", "fetch-setup-origin")
git(remote, "checkout", "-f", "master")
git(remote, "checkout", "-b", "new_branch")
writeIn(remote, "file1")
addCommitIn(remote, "Commits file 1", "file1")
git("remote", "add", "origin", File(remote, ".git").absolutePath)
git("branch", "--set-upstream-to=origin/master", "master")
return true
}
fun pull(): Boolean {
resetFiles()
write("local_file")
addCommit("Initial local commit", "local_file")
val remote = siblingRepo("origin")
git("remote", "add", "pull-setup-origin", File(remote, ".git").absolutePath)
git("push", "pull-setup-origin", "master")
git("remote", "remove", "pull-setup-origin")
git(remote, "checkout", "-f", "master")
writeIn(remote, "remote_file", "pulled from origin\n")
addCommitIn(remote, "Pulled commit", "remote_file")
git("remote", "add", "origin", File(remote, ".git").absolutePath)
git("config", "branch.master.remote", "origin")
git("config", "branch.master.merge", "refs/heads/master")
return true
}
fun pushBranch(): Boolean {
resetFiles()
write("file1")
addCommit("committed changes on master", "file1")
val remote = siblingRepo("origin")
git("remote", "add", "push-branch-setup-origin", File(remote, ".git").absolutePath)
git("push", "push-branch-setup-origin", "master")
git("remote", "remove", "push-branch-setup-origin")
git(remote, "checkout", "-f", "master")
write("file2")
addCommit("If this commit gets pushed to repo, then you have lost the level :( ", "file2")
checkoutNew("other_branch")
write("file3")
addCommit("If this commit gets pushed to repo, then you have lost the level :( ", "file3")
checkoutNew("test_branch")
write("file4")
addCommit("committed change on test_branch", "file4")
git("remote", "add", "origin", File(remote, ".git").absolutePath)
git("branch", "--set-upstream-to=origin/master", "master")
checkout("master")
return true
}
fun pushTags(): Boolean {
resetFiles()
write("file1")
addCommit("First commit", "file1")
tag("tag_to_be_pushed")
write("file2")
addCommit("Second commit", "file2")
val remote = siblingRepo("origin")
git("remote", "add", "push-tags-setup-origin", File(remote, ".git").absolutePath)
git("push", "push-tags-setup-origin", "master")
git("remote", "remove", "push-tags-setup-origin")
git(remote, "checkout", "-f", "master")
git("remote", "add", "origin", File(remote, ".git").absolutePath)
return true
}
fun merge(): Boolean {
resetFiles()
write("file1")
addCommit("added file1", "file1")
checkoutNew("feature")
write("file2")
addCommit("added file2", "file2")
checkout("master")
return true
}
fun rebase(): Boolean {
resetFiles()
write("README", "readme\n")
addCommit("init commit", "README")
checkoutNew("feature")
write("feature", "feature\n")
addCommit("add feature", "feature")
checkout("master")
append("README", "content\n")
addCommit("add content", "README")
return true
}
fun rebaseOnto(): Boolean {
resetFiles()
write("authors.md", "https://github.com/janis-vitols\n")
addCommit("Create authors file", "authors.md")
checkoutNew("wrong_branch")
write("authors.md", "None\n")
addCommit("Wrong changes", "authors.md")
checkoutNew("readme-update")
write("README.md", "# SuperApp\n")
addCommit("Add app name in readme", "README.md")
append("README.md", "## About\n")
addCommit("Add `About` header in readme", "README.md")
append("README.md", "## Install\n")
addCommit("Add `Install` header in readme", "README.md")
return true
}
fun mergeSquash(): Boolean {
resetFiles()
write("file1")
addCommit("First commit", "file1")
checkoutNew("long-feature-branch")
write("file3", "some feature\n")
addCommit("Developing new features", "file3")
append("file3", "getting awesomer\n")
addCommit("Takes", "file3")
append("file3", "and awesomer!\n")
addCommit("Time", "file3")
checkout("master")
write("file2")
addCommit("Second commit", "file2")
return true
}
fun reset(): Boolean {
resetFiles()
write("README")
addCommit("Initial commit", "README")
write("to_commit_first.rb")
write("to_commit_second.rb")
add("to_commit_first.rb", "to_commit_second.rb")
return true
}
fun resetSoft(): Boolean {
resetFiles()
write("README")
addCommit("Initial commit", "README")
write("newfile.rb")
addCommit("Premature commit", "newfile.rb")
return true
}
fun restore(): Boolean {
resetFiles()
write("file1")
addCommit("Initial commit", "file1")
write("file2")
addCommit("First commit", "file2")
write("file3")
addCommit("Restore this commit", "file3")
git("reset", "--hard", "HEAD^")
return true
}
fun squash(): Boolean {
resetFiles()
write(".hidden")
addCommit("Initial Commit", ".hidden")
write("README")
addCommit("Adding README", "README")
write("README", "hey there")
addCommit("Updating README (squash this commit into Adding README)", "README")
append("README", "\nAdding some more text")
addCommit("Updating README (squash this commit into Adding README)", "README")
append("README", "\neven more text")
addCommit("Updating README (squash this commit into Adding README)", "README")
return true
}
fun reorder(): Boolean {
resetFiles()
write("README")
addCommit("Initial Setup", "README")
write("file1")
addCommit("First commit", "file1")
write("file3")
addCommit("Third commit", "file3")
write("file2")
addCommit("Second commit", "file2")
return true
}
fun renameCommit(): Boolean {
resetFiles()
write("README")
addCommit("Initial commit", "README")
write("file1")
addCommit("First coommit", "file1")
write("file2")
addCommit("Second commit", "file2")
return true
}
fun revert(): Boolean {
resetFiles()
write("file1")
addCommit("First commit", "file1")
write("file3")
addCommit("Bad commit", "file3")
write("file2")
addCommit("Second commit", "file2")
return true
}
fun stash(): Boolean {
resetFiles()
write(
"lyrics.txt",
"""
Down in Louisiana in that sunny clime,
They play a class of music that is super fine,
And it makes no difference if its rain or shine,
You can hear that that jazz band music playing all the time.
""".trimIndent() + "\n",
)
addCommit("Add some lyrics", "lyrics.txt")
append("lyrics.txt", "\nHey!\n")
return true
}
}