From 64743ff4a95e38ec98e5b9882ab818c03793ff65 Mon Sep 17 00:00:00 2001 From: Joe Tretter Date: Thu, 7 May 2026 18:53:18 -0500 Subject: [PATCH] Move native level setups into level files - Delete NativeLevelFixtures.kt and make native repository setup an optional property of each Level. - Move every existing native setup into its corresponding level file so setup, validation, and tests stay together. - Add the missing Blame native setup with config.rb history and a Spider Man password commit. - Add a native runtime regression that verifies `git blame config.rb` identifies Spider Man on the password line. - Verify the full JVM test suite using the compiled host Git runtime. --- README.md | 61 ++- app/build.gradle.kts | 4 +- .../tretter/githugandroid/GameModels.kt | 87 ++++ .../tretter/githugandroid/GitRuntime.kt | 72 +-- .../githugandroid/NativeLevelFixtures.kt | 434 ------------------ .../githugandroid/levels/BlameLevel.kt | 50 +- .../githugandroid/levels/BranchAtLevel.kt | 10 + .../githugandroid/levels/CheckoutFileLevel.kt | 7 + .../githugandroid/levels/CheckoutTagLevel.kt | 21 + .../levels/CheckoutTagOverBranchLevel.kt | 8 + .../tretter/githugandroid/levels/DiffLevel.kt | 43 ++ .../githugandroid/levels/FetchLevel.kt | 18 + .../githugandroid/levels/LevelCatalog.kt | 2 + .../githugandroid/levels/MergeLevel.kt | 10 + .../githugandroid/levels/MergeSquashLevel.kt | 16 + .../tretter/githugandroid/levels/PullLevel.kt | 18 + .../githugandroid/levels/PushBranchLevel.kt | 24 + .../tretter/githugandroid/levels/PushLevel.kt | 45 ++ .../githugandroid/levels/PushTagsLevel.kt | 17 + .../githugandroid/levels/RebaseLevel.kt | 12 + .../githugandroid/levels/RebaseOntoLevel.kt | 16 + .../githugandroid/levels/RenameCommitLevel.kt | 10 + .../githugandroid/levels/ReorderLevel.kt | 12 + .../githugandroid/levels/ResetLevel.kt | 9 + .../githugandroid/levels/ResetSoftLevel.kt | 8 + .../githugandroid/levels/RestoreLevel.kt | 11 + .../githugandroid/levels/RevertLevel.kt | 10 + .../githugandroid/levels/SquashLevel.kt | 14 + .../githugandroid/levels/StashLevel.kt | 15 + .../githugandroid/GitSandboxEngineTest.kt | 21 + 30 files changed, 580 insertions(+), 505 deletions(-) delete mode 100644 app/src/main/java/solutions/tretter/githugandroid/NativeLevelFixtures.kt diff --git a/README.md b/README.md index f326fcf..ae8b40c 100644 --- a/README.md +++ b/README.md @@ -39,16 +39,34 @@ It is designed to be idempotent and uses project-relative paths to: - install required Android SDK packages - write `local.properties` +Available commands: + +| Command | Purpose | Output | +| --- | --- | --- | +| `bash ./AndroidProjectTooling.sh` | Provision or refresh the local Android/JDK toolchain only. | Toolchain under `./jdk` and `./android-sdk` | +| `bash ./AndroidProjectTooling.sh --test` | Compile host Git, set `GITHUG_TEST_GIT_BINARY`, and run JVM unit tests. | Test reports under `app/build/reports/` | +| `bash ./AndroidProjectTooling.sh --build` | Build the debug APK. | `app/build/outputs/apk/debug/githug-android-debug.apk` | +| `bash ./AndroidProjectTooling.sh --build-aab` | Build the debug Android App Bundle. | `app/build/outputs/bundle/debug/githug-android-debug.aab` | +| `bash ./AndroidProjectTooling.sh --build-release-aab` | Build the release Android App Bundle. | `app/build/outputs/bundle/release/githug-android-release.aab` | +| `bash ./AndroidProjectTooling.sh --compile-git` | Compile Git for the development host and all Android target ABIs. | Host and Android `libgit.so` binaries | + To run the JVM unit test suite after ensuring the local toolchain is ready: ```bash bash ./AndroidProjectTooling.sh --test ``` -To build the debug APK: +To build installable/debuggable artifacts: ```bash bash ./AndroidProjectTooling.sh --build +bash ./AndroidProjectTooling.sh --build-aab +``` + +To build the release AAB intended for Play Store style distribution work: + +```bash +bash ./AndroidProjectTooling.sh --build-release-aab ``` The script avoids depending on the system Java version by provisioning a project-local **JDK 17**, which is required by current Android SDK command-line tools. @@ -59,13 +77,50 @@ To avoid repeated expensive SDK verification, the script writes a small state fi If all required components were verified successfully, the script will skip SDK verification/reinstallation for the next **24 hours** unless required directories are missing. -When `--build` is used, the script also: +When `--build`, `--build-aab`, or `--build-release-aab` is used, the script also: - increments `versionCode` by 1 - increments the patch component of `versionName`, for example `0.1.0` to `0.1.1` -- renames the debug APK to `githug-android-debug.apk` +- renames the generated artifact to a stable `githug-android-*` filename - attempts to create a git commit after a successful build if there are source changes +The build commands currently run these Gradle tasks: + +- `--build`: `assembleDebug` +- `--build-aab`: `bundleDebug` +- `--build-release-aab`: `bundleRelease` + +## Git Binary Compilation + +Native Git is compiled with: + +```bash +bash ./CompileGitForAllTargetPlatforms.sh [--host | --android | --all] +``` + +Options: + +| Command | Purpose | Output | +| --- | --- | --- | +| `bash ./CompileGitForAllTargetPlatforms.sh --host` | Compile Git for the development machine. | `build/host-git/libgit.so` | +| `bash ./CompileGitForAllTargetPlatforms.sh --android` | Cross-compile Git for Android ABIs served by Google Play. | `app/src/main/jniLibs//libgit.so` | +| `bash ./CompileGitForAllTargetPlatforms.sh --all` | Compile both host and Android targets. This is the default. | Host and Android outputs | + +Android ABI outputs: + +- `app/src/main/jniLibs/arm64-v8a/libgit.so` +- `app/src/main/jniLibs/armeabi-v7a/libgit.so` +- `app/src/main/jniLibs/x86/libgit.so` +- `app/src/main/jniLibs/x86_64/libgit.so` + +The `--test` tooling command automatically runs `CompileGitForAllTargetPlatforms.sh --host` first and exports `GITHUG_TEST_GIT_BINARY=build/host-git/libgit.so`. This keeps JVM tests on the same `GitRepositoryRuntime` path as the app, including the packaged-runtime helper resolution behavior. + +The `--compile-git` tooling command is a convenience wrapper for: + +```bash +bash ./CompileGitForAllTargetPlatforms.sh --all +``` + ## Runtime Architecture The command engine has two execution paths behind one app-facing runtime: diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cde6281..7ec6592 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -19,8 +19,8 @@ android { applicationId = "solutions.tretter.githugandroid" minSdk = 26 targetSdk = 35 - versionCode = 127 - versionName = "0.1.126" + versionCode = 128 + versionName = "0.1.127" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true diff --git a/app/src/main/java/solutions/tretter/githugandroid/GameModels.kt b/app/src/main/java/solutions/tretter/githugandroid/GameModels.kt index 910e727..425ad1f 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/GameModels.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/GameModels.kt @@ -1,6 +1,7 @@ package solutions.tretter.githugandroid import androidx.compose.runtime.saveable.listSaver +import java.io.File enum class PlayMode(val label: String) { CLI_ONLY("CLI ONLY"), @@ -46,6 +47,7 @@ data class Level( val commandSuggestions: List, val validator: (RepoState, String) -> Boolean, val setup: () -> RepoState, + val nativeSetup: (NativeLevelSetup.() -> Boolean)? = null, val testCases: List = emptyList(), ) @@ -54,6 +56,91 @@ data class LevelTestCase( val commands: List, ) +class NativeLevelSetup internal constructor( + internal val sandbox: File, + private val runGit: (File, List) -> Int, +) { + fun resetFiles() { + sandbox.listFiles() + ?.filterNot { it.name == ".git" } + ?.forEach { it.deleteRecursively() } + git("checkout", "-B", "master") + } + + fun git(vararg arguments: String): Int = runGit(sandbox, arguments.toList()) + + fun git(directory: File, vararg arguments: String): Int = runGit(directory, arguments.toList()) + + 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") + } + + fun write(path: String, content: String = "") { + File(sandbox, path).apply { + parentFile?.mkdirs() + writeText(content) + } + } + + fun append(path: String, content: String) { + File(sandbox, path).appendText(content) + } + + fun add(vararg paths: String) { + git("add", *paths) + } + + fun commit(message: String, author: String? = null) { + if (author == null) { + git("commit", "-m", message) + } else { + git("commit", "--author", author, "-m", message) + } + } + + fun addCommit(message: String, vararg paths: String, author: String? = null) { + add(*paths) + commit(message, author) + } + + fun writeIn(directory: File, path: String, content: String = "") { + File(directory, path).apply { + parentFile?.mkdirs() + writeText(content) + } + } + + fun addCommitIn(directory: File, message: String, vararg paths: String) { + git(directory, "add", *paths) + git(directory, "commit", "-m", message) + } + + fun siblingRepo(name: String): File { + val directory = File(sandbox.parentFile ?: sandbox, "${sandbox.name}-$name") + directory.deleteRecursively() + initRepo(directory) + return directory + } + + fun checkoutNew(branch: String) { + git("checkout", "-b", branch) + } + + fun checkout(branch: String) { + git("checkout", branch) + } + + fun tag(name: String) { + git("tag", "-f", name) + } +} + fun sampleLevels(): List = allGithugLevels() val RepoStateSaver = listSaver( diff --git a/app/src/main/java/solutions/tretter/githugandroid/GitRuntime.kt b/app/src/main/java/solutions/tretter/githugandroid/GitRuntime.kt index 4283e53..5743464 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/GitRuntime.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/GitRuntime.kt @@ -90,7 +90,7 @@ class GitRepositoryRuntime private constructor( runGit(nativeGit, sandbox, listOf("checkout", "-B", desired.headBranch)) } - materializeNativeGitState(nativeGit, sandbox, desired, level.id) + materializeNativeGitState(nativeGit, sandbox, desired, level) } return inspectSandbox(level) @@ -443,16 +443,14 @@ class GitRepositoryRuntime private constructor( return body } - private fun materializeNativeGitState(nativeGit: File, sandbox: File, desired: RepoState, levelId: String) { - if (levelId == "push") { - materializeNativePushLevel(nativeGit, sandbox) - return - } - if (materializeNativeLevelFixture(levelId, sandbox) { directory, arguments -> + 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 } - ) { - return + if (nativeSetup.setup()) { + return + } } desired.config.forEach { (key, value) -> @@ -506,62 +504,6 @@ 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 parent = sandbox.parentFile ?: sandbox - val remoteDir = File(parent, "${sandbox.name}-origin.git") - val remoteWorkTree = File(parent, "${sandbox.name}-origin-work") - remoteDir.deleteRecursively() - remoteWorkTree.deleteRecursively() - remoteDir.mkdirs() - remoteWorkTree.mkdirs() - runGit(nativeGit, remoteDir, listOf("init", "--bare", "-b", "master")) - runGit(nativeGit, sandbox, listOf("remote", "add", "push-setup-origin", remoteDir.absolutePath)) - runGit(nativeGit, sandbox, listOf("push", "push-setup-origin", "master")) - runGit(nativeGit, sandbox, listOf("remote", "remove", "push-setup-origin")) - - fun runRemoteGit(arguments: List): ProcessExecutionResult { - return runGit( - nativeGit, - sandbox, - listOf("--git-dir", remoteDir.absolutePath, "--work-tree", remoteWorkTree.absolutePath) + arguments, - ) - } - - runRemoteGit(listOf("checkout", "-f", "master")) - File(remoteWorkTree, "file4").writeText("file4\n") - runRemoteGit(listOf("add", "file4")) - runRemoteGit(listOf("commit", "-m", "Fourth commit")) - - writeFile("file3") - commitIn(sandbox, "Third commit", "file3") - runGit(nativeGit, sandbox, listOf("remote", "add", "origin", remoteDir.absolutePath)) - runGit(nativeGit, sandbox, listOf("fetch", "origin")) - runGit(nativeGit, sandbox, listOf("branch", "--set-upstream-to=origin/master", "master")) - runGit(nativeGit, sandbox, listOf("config", "rebase.autoStash", "true")) - File(sandbox, "file3").appendText("local worktree change\n") - } - private fun filesForSetupCommit(files: List, index: Int, commitCount: Int): List { if (files.isEmpty()) return emptyList() if (commitCount <= 1) return files diff --git a/app/src/main/java/solutions/tretter/githugandroid/NativeLevelFixtures.kt b/app/src/main/java/solutions/tretter/githugandroid/NativeLevelFixtures.kt deleted file mode 100644 index 091a7d8..0000000 --- a/app/src/main/java/solutions/tretter/githugandroid/NativeLevelFixtures.kt +++ /dev/null @@ -1,434 +0,0 @@ -package solutions.tretter.githugandroid - -import java.io.File - -internal fun materializeNativeLevelFixture( - levelId: String, - sandbox: File, - runGit: (File, List) -> Int, -): Boolean { - val fixture = NativeLevelFixture(sandbox, runGit) - return when (levelId) { - "branch_at" -> fixture.branchAt() - "checkout_tag" -> fixture.checkoutTag() - "checkout_tag_over_branch" -> fixture.checkoutTagOverBranch() - "diff" -> fixture.diff() - "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) -> 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 diff(): Boolean { - resetFiles() - write("app.rb", diffLevelBaselineAppRb()) - addCommit("Add app routes", "app.rb") - write("app.rb", diffLevelModifiedAppRb()) - 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 - } -} - -internal fun diffLevelBaselineAppRb(): String = buildString { - appendLine("require 'sinatra'") - appendLine("require 'json'") - appendLine() - appendLine("helpers do") - appendLine(" def get_response(source)") - appendLine(" JSON.parse(File.read(source))['message']") - appendLine(" end") - appendLine("end") - appendLine() - appendLine("get '/' do") - appendLine(" @message = 'hello'") - appendLine(" erb :index") - appendLine("end") - appendLine() - appendLine("get '/page' do") - appendLine(" @message = 'page'") - appendLine(" erb :page") - appendLine("end") - appendLine() - appendLine("get '/yet_another' do") - appendLine(" @message = 'another'") - appendLine(" erb :success") - appendLine("end") - appendLine() - appendLine("get '/another_page' do") - appendLine(" @message = get_response('data.json')") - appendLine(" erb :another") - appendLine("end") - appendLine() - appendLine("# end of application") -} - -internal fun diffLevelModifiedAppRb(): String = - diffLevelBaselineAppRb().replace("get_response('data.json')", "get_response('server.json')") diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/BlameLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/BlameLevel.kt index 85ee3cc..b319368 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/BlameLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/BlameLevel.kt @@ -13,9 +13,57 @@ internal fun blameLevel(): Level = level( description = "Identify who put a password inside the file `config.rb`.", hints = listOf("You want to research the `git blame` command."), commandSuggestions = listOf("git blame config.rb"), - setup = { RepoState(initialized = true, files = listOf(GitFile("config.rb", tracked = true)), branches = mapOf("master" to 1)) }, + setup = { + RepoState( + initialized = true, + files = listOf(GitFile("config.rb", blameLevelFinalConfigRb(), tracked = true)), + commits = listOf( + CommitNode("0000001", "Add default config"), + CommitNode("0000002", "Add secret config"), + CommitNode("0000003", "Document timeout"), + ), + branches = mapOf("master" to 3), + ) + }, + nativeSetup = { + resetFiles() + write( + "config.rb", + """ + Githug::Application.configure do + config.cache_classes = true + config.log_level = :info + end + """.trimIndent() + "\n", + ) + addCommit("Add default config", "config.rb", author = "Peter Parker ") + write( + "config.rb", + """ + Githug::Application.configure do + config.cache_classes = true + config.log_level = :info + config.password = "correct horse battery staple" + end + """.trimIndent() + "\n", + ) + addCommit("Add secret config", "config.rb", author = "Spider Man ") + append("config.rb", "config.timeout = 30\n") + addCommit("Document timeout", "config.rb", author = "Mary Jane ") + true + }, validator = commandAnswer("Spider Man"), testCases = listOf( levelTestCase("answer author", "Spider Man"), ), ) + +private fun blameLevelFinalConfigRb(): String = + """ + Githug::Application.configure do + config.cache_classes = true + config.log_level = :info + config.password = "correct horse battery staple" + end + config.timeout = 30 + """.trimIndent() + "\n" diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/BranchAtLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/BranchAtLevel.kt index a43156a..2c2f541 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/BranchAtLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/BranchAtLevel.kt @@ -14,6 +14,16 @@ internal fun branchAtLevel(): Level = level( hints = listOf("Just like creating a branch, but you have to pass an extra argument."), commandSuggestions = listOf("git branch test_branch HEAD~1"), setup = { RepoState(initialized = true, commits = listOf(CommitNode("1", "Adding file1"), CommitNode("2", "Updating file1"), CommitNode("3", "Updating file1 again")), branches = mapOf("master" to 3)) }, + nativeSetup = { + 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") + true + }, validator = repoPredicate { repo -> repo.branches["test_branch"] == 2 }, testCases = listOf( levelTestCase("branch at previous commit", "git branch test_branch HEAD~1"), diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/CheckoutFileLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/CheckoutFileLevel.kt index a956a5c..308a826 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/CheckoutFileLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/CheckoutFileLevel.kt @@ -14,6 +14,13 @@ internal fun checkoutFileLevel(): Level = level( hints = listOf("You will need to do some research on the checkout command for this one."), commandSuggestions = listOf("git checkout -- config.rb"), setup = { RepoState(initialized = true, files = listOf(GitFile("config.rb", "This is the initial config file\nThese are changes you don't want to keep!", tracked = true)), commits = listOf(CommitNode("0000001", "Added initial config file")), branches = mapOf("master" to 1)) }, + nativeSetup = { + 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!") + true + }, validator = repoPredicate { repo -> repo.files.find { it.name == "config.rb" }?.content == "This is the initial config file" }, testCases = listOf( levelTestCase("checkout file from head", "git checkout -- config.rb"), diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/CheckoutTagLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/CheckoutTagLevel.kt index d053b9f..9cada9a 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/CheckoutTagLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/CheckoutTagLevel.kt @@ -14,9 +14,30 @@ internal fun checkoutTagLevel(): Level = level( hints = listOf("There's no big difference between checking out a branch and checking out a tag."), commandSuggestions = listOf("git checkout v1.2"), setup = { RepoState(initialized = true, commits = listOf(CommitNode("1", "Initial commit"), CommitNode("2", "Some changes"), CommitNode("3", "Some more changes"), CommitNode("4", "Yet more changes"), CommitNode("5", "Changes galore")), tags = listOf("v1.0", "v1.2", "v1.5"), branches = mapOf("master" to 5)) }, + nativeSetup = { + checkoutTagHistory() + true + }, validator = repoPredicate { repo -> repo.headBranch == "tags/v1.2" || repo.branches.keys.any { "detached at v1.2" in it } }, testCases = listOf( levelTestCase("checkout tag", "git checkout v1.2"), levelTestCase("checkout explicit tag", "git checkout tags/v1.2"), ), ) + +internal fun NativeLevelSetup.checkoutTagHistory() { + 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") +} diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/CheckoutTagOverBranchLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/CheckoutTagOverBranchLevel.kt index 214cbd7..6691958 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/CheckoutTagOverBranchLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/CheckoutTagOverBranchLevel.kt @@ -14,6 +14,14 @@ internal fun checkoutTagOverBranchLevel(): Level = level( hints = listOf("You should think about specifying you're after the tag named `v1.2` (think `tags/`)."), commandSuggestions = listOf("git checkout tags/v1.2"), setup = { RepoState(initialized = true, tags = listOf("v1.0", "v1.2", "v1.5"), branches = mapOf("master" to 5, "v1.2" to 6)) }, + nativeSetup = { + checkoutTagHistory() + checkoutNew("v1.2") + write("file3", "some feature\n") + addCommit("Developing new features", "file3") + checkout("master") + true + }, validator = repoPredicate { repo -> repo.headBranch == "tags/v1.2" || repo.branches.keys.any { "detached at v1.2" in it } }, testCases = listOf( levelTestCase("checkout tag namespace", "git checkout tags/v1.2"), diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/DiffLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/DiffLevel.kt index ecc8157..22ae080 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/DiffLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/DiffLevel.kt @@ -21,8 +21,51 @@ internal fun diffLevel(): Level = level( branches = mapOf("master" to 1), ) }, + nativeSetup = { + resetFiles() + write("app.rb", diffLevelBaselineAppRb()) + addCommit("Add app routes", "app.rb") + write("app.rb", diffLevelModifiedAppRb()) + true + }, validator = commandAnswer("26"), testCases = listOf( levelTestCase("answer changed line", "26"), ), ) + +internal fun diffLevelBaselineAppRb(): String = buildString { + appendLine("require 'sinatra'") + appendLine("require 'json'") + appendLine() + appendLine("helpers do") + appendLine(" def get_response(source)") + appendLine(" JSON.parse(File.read(source))['message']") + appendLine(" end") + appendLine("end") + appendLine() + appendLine("get '/' do") + appendLine(" @message = 'hello'") + appendLine(" erb :index") + appendLine("end") + appendLine() + appendLine("get '/page' do") + appendLine(" @message = 'page'") + appendLine(" erb :page") + appendLine("end") + appendLine() + appendLine("get '/yet_another' do") + appendLine(" @message = 'another'") + appendLine(" erb :success") + appendLine("end") + appendLine() + appendLine("get '/another_page' do") + appendLine(" @message = get_response('data.json')") + appendLine(" erb :another") + appendLine("end") + appendLine() + appendLine("# end of application") +} + +internal fun diffLevelModifiedAppRb(): String = + diffLevelBaselineAppRb().replace("get_response('data.json')", "get_response('server.json')") diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/FetchLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/FetchLevel.kt index 87ea09f..1084f5a 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/FetchLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/FetchLevel.kt @@ -1,5 +1,7 @@ package solutions.tretter.githugandroid +import java.io.File + /** * Port of the upstream ruby-githug `fetch` level. * @@ -14,6 +16,22 @@ internal fun fetchLevel(): Level = level( hints = listOf("Look up the 'git fetch' command"), commandSuggestions = listOf("git fetch origin"), setup = { RepoState(initialized = true, branches = mapOf("master" to 1), remotes = mapOf("origin" to "remote")) }, + nativeSetup = { + 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") + true + }, validator = repoPredicate { repo -> "origin/new_branch" in repo.fetchedBranches && repo.headBranch == "master" }, testCases = listOf( levelTestCase("fetch origin", "git fetch origin"), diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/LevelCatalog.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/LevelCatalog.kt index b0f2a9a..65c62a2 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/LevelCatalog.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/LevelCatalog.kt @@ -68,6 +68,7 @@ internal fun level( setup: () -> RepoState, validator: (RepoState, String) -> Boolean, testCases: List = emptyList(), + nativeSetup: (NativeLevelSetup.() -> Boolean)? = null, ): Level = Level( id = id, title = title, @@ -76,6 +77,7 @@ internal fun level( commandSuggestions = commandSuggestions, validator = loggingValidator(id, title, validator), setup = setup, + nativeSetup = nativeSetup, testCases = testCases, ) diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/MergeLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/MergeLevel.kt index 95290da..ff6ff86 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/MergeLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/MergeLevel.kt @@ -14,6 +14,16 @@ internal fun mergeLevel(): Level = level( hints = listOf("You want to research the `git merge` command."), commandSuggestions = listOf("git merge feature"), setup = { RepoState(initialized = true, files = listOf(GitFile("file1", tracked = true)), branches = mapOf("master" to 1, "feature" to 2)) }, + nativeSetup = { + resetFiles() + write("file1") + addCommit("added file1", "file1") + checkoutNew("feature") + write("file2") + addCommit("added file2", "file2") + checkout("master") + true + }, validator = repoPredicate { repo -> repo.files.any { it.name == "file2" && it.tracked } }, testCases = listOf( levelTestCase("merge feature", "git merge feature"), diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/MergeSquashLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/MergeSquashLevel.kt index c6631df..f2c24de 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/MergeSquashLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/MergeSquashLevel.kt @@ -14,6 +14,22 @@ internal fun mergeSquashLevel(): Level = level( hints = listOf("Take a look at the `--squash` option of the merge command. Don't forget to commit the merge!"), commandSuggestions = listOf("git merge --squash long-feature-branch", "git commit -m \"Merge long feature\""), setup = { RepoState(initialized = true, files = listOf(GitFile("file1", tracked = true)), branches = mapOf("master" to 2, "long-feature-branch" to 4)) }, + nativeSetup = { + 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") + true + }, validator = repoPredicate { repo -> "merge-squash" in repo.maintenanceActions && repo.commits.isNotEmpty() }, testCases = listOf( levelTestCase("squash merge then commit", "git merge --squash long-feature-branch", "git commit -m \"Merge long feature\""), diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/PullLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/PullLevel.kt index d05d360..89a0aec 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/PullLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/PullLevel.kt @@ -1,5 +1,7 @@ package solutions.tretter.githugandroid +import java.io.File + /** * Port of the upstream ruby-githug `pull` level. * @@ -14,6 +16,22 @@ internal fun pullLevel(): Level = level( hints = listOf("Check out the remote repositories and research `git pull`."), commandSuggestions = listOf("git pull origin master"), setup = { RepoState(initialized = true, remotes = mapOf("origin" to "remote"), branches = mapOf("master" to 1)) }, + nativeSetup = { + 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") + true + }, validator = repoPredicate { repo -> "origin/master" in repo.fetchedBranches && repo.files.any { it.name == "remote_file" && it.tracked } && diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/PushBranchLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/PushBranchLevel.kt index cf78048..016d0ed 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/PushBranchLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/PushBranchLevel.kt @@ -1,5 +1,7 @@ package solutions.tretter.githugandroid +import java.io.File + /** * Port of the upstream ruby-githug `push_branch` level. * @@ -14,6 +16,28 @@ internal fun pushBranchLevel(): Level = level( hints = listOf("Investigate the options in `git push` using `git push --help`"), commandSuggestions = listOf("git push origin test_branch"), setup = { RepoState(initialized = true, branches = mapOf("master" to 2, "other_branch" to 3, "test_branch" to 4), remotes = mapOf("origin" to "remote"), headBranch = "master") }, + nativeSetup = { + 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") + true + }, validator = repoPredicate { repo -> "origin/test_branch" in repo.pushedBranches && "origin/master" !in repo.pushedBranches && "origin/other_branch" !in repo.pushedBranches }, testCases = listOf( levelTestCase("push named branch", "git push origin test_branch"), diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/PushLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/PushLevel.kt index 89a693e..8abda26 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/PushLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/PushLevel.kt @@ -1,5 +1,7 @@ package solutions.tretter.githugandroid +import java.io.File + /** * Port of the upstream ruby-githug `push` level. * @@ -31,6 +33,49 @@ internal fun pushLevel(): Level = level( fetchedBranches = setOf("origin/master"), ) }, + nativeSetup = { + resetFiles() + write("file1", "file1\n") + addCommit("First commit", "file1") + write("file2", "file2\n") + addCommit("Second commit", "file2") + + val parent = sandbox.parentFile ?: sandbox + val remoteDir = File(parent, "${sandbox.name}-origin.git") + val remoteWorkTree = File(parent, "${sandbox.name}-origin-work") + remoteDir.deleteRecursively() + remoteWorkTree.deleteRecursively() + remoteDir.mkdirs() + remoteWorkTree.mkdirs() + git(remoteDir, "init", "--bare", "-b", "master") + git("remote", "add", "push-setup-origin", remoteDir.absolutePath) + git("push", "push-setup-origin", "master") + git("remote", "remove", "push-setup-origin") + + fun remoteGit(vararg arguments: String) { + git( + "--git-dir", + remoteDir.absolutePath, + "--work-tree", + remoteWorkTree.absolutePath, + *arguments, + ) + } + + remoteGit("checkout", "-f", "master") + File(remoteWorkTree, "file4").writeText("file4\n") + remoteGit("add", "file4") + remoteGit("commit", "-m", "Fourth commit") + + write("file3", "file3\n") + addCommit("Third commit", "file3") + git("remote", "add", "origin", remoteDir.absolutePath) + git("fetch", "origin") + git("branch", "--set-upstream-to=origin/master", "master") + git("config", "rebase.autoStash", "true") + append("file3", "local worktree change\n") + true + }, validator = repoPredicate { repo -> "origin/master" in repo.pushedBranches && repo.commits.size >= 4 && diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/PushTagsLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/PushTagsLevel.kt index 75e1e00..42c9f71 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/PushTagsLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/PushTagsLevel.kt @@ -1,5 +1,7 @@ package solutions.tretter.githugandroid +import java.io.File + /** * Port of the upstream ruby-githug `push_tags` level. * @@ -14,6 +16,21 @@ internal fun pushTagsLevel(): Level = level( hints = listOf("Take a look at `--tags` flag of `git push`"), commandSuggestions = listOf("git push --tags"), setup = { RepoState(initialized = true, tags = listOf("tag_to_be_pushed"), branches = mapOf("master" to 2), remotes = mapOf("origin" to "remote")) }, + nativeSetup = { + 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) + true + }, validator = repoPredicate { repo -> "tag_to_be_pushed" in repo.pushedTags }, testCases = listOf( levelTestCase("push all tags", "git push --tags"), diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/RebaseLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/RebaseLevel.kt index 7ab5364..e5aa837 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/RebaseLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/RebaseLevel.kt @@ -14,6 +14,18 @@ internal fun rebaseLevel(): Level = level( hints = listOf("You want to research the `git rebase` command"), commandSuggestions = listOf("git checkout feature", "git rebase master"), setup = { RepoState(initialized = true, headBranch = "master", branches = mapOf("master" to 2, "feature" to 2)) }, + nativeSetup = { + 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") + true + }, validator = repoPredicate { repo -> repo.headBranch == "feature" && repo.commits.take(3).map { it.message } == listOf("add feature", "add content", "init commit") diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/RebaseOntoLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/RebaseOntoLevel.kt index 2cc6e02..7721cf4 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/RebaseOntoLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/RebaseOntoLevel.kt @@ -14,6 +14,22 @@ internal fun rebaseOntoLevel(): Level = level( hints = listOf("You want to research the `git rebase` commands `--onto` argument"), commandSuggestions = listOf("git rebase --onto master wrong_branch readme-update"), setup = { RepoState(initialized = true, headBranch = "readme-update", branches = mapOf("master" to 1, "wrong_branch" to 2, "readme-update" to 4)) }, + nativeSetup = { + 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") + true + }, validator = repoPredicate { repo -> repo.headBranch == "readme-update" && "rebase-onto" in repo.maintenanceActions }, testCases = listOf( levelTestCase("rebase onto explicit branch", "git rebase --onto master wrong_branch readme-update"), diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/RenameCommitLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/RenameCommitLevel.kt index fc7f31a..8f0271d 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/RenameCommitLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/RenameCommitLevel.kt @@ -14,6 +14,16 @@ internal fun renameCommitLevel(): Level = level( hints = listOf("Take a look the `-i` flag of the rebase command."), commandSuggestions = listOf("git rebase -i HEAD~2"), setup = { RepoState(initialized = true, commits = listOf(CommitNode("1", "Initial commit"), CommitNode("2", "First coommit"), CommitNode("3", "Second commit")), branches = mapOf("master" to 3)) }, + nativeSetup = { + resetFiles() + write("README") + addCommit("Initial commit", "README") + write("file1") + addCommit("First coommit", "file1") + write("file2") + addCommit("Second commit", "file2") + true + }, validator = repoPredicate { repo -> repo.commits.any { it.message == "First commit" } && repo.commits.none { it.message.contains("coommit") } }, testCases = listOf( levelTestCase("interactive rebase rename", "git rebase -i HEAD~2"), diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/ReorderLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/ReorderLevel.kt index d9303c9..930d1b9 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/ReorderLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/ReorderLevel.kt @@ -14,6 +14,18 @@ internal fun reorderLevel(): Level = level( hints = listOf("Take a look the `-i` flag of the rebase command."), commandSuggestions = listOf("git rebase -i HEAD~3"), setup = { RepoState(initialized = true, commits = listOf(CommitNode("1", "Initial Setup"), CommitNode("2", "First commit"), CommitNode("3", "Third commit"), CommitNode("4", "Second commit")), branches = mapOf("master" to 4)) }, + nativeSetup = { + 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") + true + }, validator = repoPredicate { repo -> repo.commits.map { it.message }.filter { it.endsWith("commit") } == listOf("First commit", "Second commit", "Third commit") }, testCases = listOf( levelTestCase("interactive reorder", "git rebase -i HEAD~3"), diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/ResetLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/ResetLevel.kt index 5190c99..07bfefa 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/ResetLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/ResetLevel.kt @@ -14,6 +14,15 @@ internal fun resetLevel(): Level = level( hints = listOf("git status will tell you the command you need to run."), commandSuggestions = listOf("git reset to_commit_second.rb"), setup = { RepoState(initialized = true, files = listOf(GitFile("to_commit_first.rb", staged = true), GitFile("to_commit_second.rb", staged = true), GitFile("README", tracked = true)), commits = listOf(CommitNode("0000001", "Initial commit")), branches = mapOf("master" to 1)) }, + nativeSetup = { + 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") + true + }, validator = repoPredicate { repo -> repo.commits.size == 1 && repo.files.any { it.name == "to_commit_first.rb" && it.staged } && repo.files.any { it.name == "to_commit_second.rb" && !it.staged } }, testCases = listOf( levelTestCase("reset path", "git reset to_commit_second.rb"), diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/ResetSoftLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/ResetSoftLevel.kt index f7ac93e..6018755 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/ResetSoftLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/ResetSoftLevel.kt @@ -14,6 +14,14 @@ internal fun resetSoftLevel(): Level = level( hints = listOf("What are some options you can use with `git reset`?"), commandSuggestions = listOf("git reset --soft HEAD^"), setup = { RepoState(initialized = true, files = listOf(GitFile("README", tracked = true), GitFile("newfile.rb", tracked = true)), commits = listOf(CommitNode("0000001", "Initial commit"), CommitNode("0000002", "Premature commit")), branches = mapOf("master" to 2)) }, + nativeSetup = { + resetFiles() + write("README") + addCommit("Initial commit", "README") + write("newfile.rb") + addCommit("Premature commit", "newfile.rb") + true + }, validator = repoPredicate { repo -> repo.commits.size == 1 && repo.files.any { it.name == "newfile.rb" && it.staged } }, testCases = listOf( levelTestCase("soft reset caret", "git reset --soft HEAD^"), diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/RestoreLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/RestoreLevel.kt index 24756b2..c340389 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/RestoreLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/RestoreLevel.kt @@ -14,6 +14,17 @@ internal fun restoreLevel(): Level = level( hints = listOf("The commit is still floating around somewhere. Have you checked out `git reflog`?"), commandSuggestions = listOf("git checkout HEAD@{1} -- file3"), setup = { RepoState(initialized = true, files = listOf(GitFile("file1", tracked = true), GitFile("file2", tracked = true)), commits = listOf(CommitNode("1", "Initial commit"), CommitNode("2", "First commit")), branches = mapOf("master" to 2)) }, + nativeSetup = { + resetFiles() + write("file1") + addCommit("Initial commit", "file1") + write("file2") + addCommit("First commit", "file2") + write("file3") + addCommit("Restore this commit", "file3") + git("reset", "--hard", "HEAD^") + true + }, validator = repoPredicate { repo -> repo.files.any { it.name == "file3" && it.tracked } }, testCases = listOf( levelTestCase("checkout file from reflog commit", "git checkout HEAD@{1} -- file3"), diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/RevertLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/RevertLevel.kt index bc1807a..0a6d11c 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/RevertLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/RevertLevel.kt @@ -14,6 +14,16 @@ internal fun revertLevel(): Level = level( hints = listOf("Try the revert command."), commandSuggestions = listOf("git revert HEAD~1"), setup = { RepoState(initialized = true, commits = listOf(CommitNode("1", "First commit"), CommitNode("2", "Bad commit"), CommitNode("3", "Second commit")), branches = mapOf("master" to 3), pushedBranches = setOf("origin/master")) }, + nativeSetup = { + resetFiles() + write("file1") + addCommit("First commit", "file1") + write("file3") + addCommit("Bad commit", "file3") + write("file2") + addCommit("Second commit", "file2") + true + }, validator = repoPredicate { repo -> repo.commits.any { it.message.startsWith("Revert") } }, testCases = listOf( levelTestCase("revert middle commit", "git revert HEAD~1"), diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/SquashLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/SquashLevel.kt index ac7d8e8..ca8ec7c 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/SquashLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/SquashLevel.kt @@ -14,6 +14,20 @@ internal fun squashLevel(): Level = level( hints = listOf("Take a look at the `-i` flag of the rebase command."), commandSuggestions = listOf("git rebase -i HEAD~4"), setup = { RepoState(initialized = true, commits = listOf(CommitNode("1", "Initial Commit"), CommitNode("2", "Adding README"), CommitNode("3", "Updating README (squash this commit into Adding README)"), CommitNode("4", "Updating README (squash this commit into Adding README)"), CommitNode("5", "Updating README (squash this commit into Adding README)")), branches = mapOf("master" to 5)) }, + nativeSetup = { + 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") + true + }, validator = repoPredicate { repo -> repo.commits.size <= 2 && repo.commits.any { it.message == "Adding README" } }, testCases = listOf( levelTestCase("interactive squash", "git rebase -i HEAD~4"), diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/StashLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/StashLevel.kt index 0ff7324..f1e7a6f 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/StashLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/StashLevel.kt @@ -14,6 +14,21 @@ internal fun stashLevel(): Level = level( hints = listOf("It's like stashing. Try finding an appropriate git command."), commandSuggestions = listOf("git stash", "git status"), setup = { RepoState(initialized = true, files = listOf(GitFile("lyrics.txt", "modified lyrics", tracked = true)), commits = listOf(CommitNode("0000001", "Initial commit")), branches = mapOf("master" to 1)) }, + nativeSetup = { + 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") + true + }, validator = repoPredicate { repo -> repo.stashes.isNotEmpty() && repo.files.none { it.staged } }, testCases = listOf( levelTestCase("stash changes", "git stash"), diff --git a/app/src/test/java/solutions/tretter/githugandroid/GitSandboxEngineTest.kt b/app/src/test/java/solutions/tretter/githugandroid/GitSandboxEngineTest.kt index fa0a39f..a4ff70c 100644 --- a/app/src/test/java/solutions/tretter/githugandroid/GitSandboxEngineTest.kt +++ b/app/src/test/java/solutions/tretter/githugandroid/GitSandboxEngineTest.kt @@ -232,6 +232,27 @@ class GitSandboxEngineTest { } } + @Test + fun nativeBlameLevelShowsPasswordAuthor() { + val git = testGitBinary() + assumeTrue(git.exists() && git.canExecute()) + val root = Files.createTempDirectory("githug-blame-level").toFile() + try { + val runtime = GitRepositoryRuntime(root, git) + val level = blameLevel() + val repo = runtime.prepareLevel(level) + + assertTrue(repo.files.single { it.name == "config.rb" }.content.contains("password")) + + val (_, blameOutput) = runtime.execute(level, repo, "git blame config.rb") + + assertTrue(blameOutput.any { it.contains("Spider Man") && it.contains("password") }) + assertTrue(level.validator(repo, "Spider Man")) + } finally { + root.deleteRecursively() + } + } + @Test fun cdDotDotShortcutMovesToParentDirectory() { val repo = RepoState(initialized = true, currentDir = "src/main")