Auto-commit after successful build: update app gameplay/UI, improve build setup, update docs/config

Changed files:\nREADME.md
app/build.gradle.kts
app/src/main/java/com/kawomi/githugandroid/GameModels.kt
app/src/main/java/com/kawomi/githugandroid/GitHugApp.kt
app/src/main/java/com/kawomi/githugandroid/GitRuntime.kt
app/src/main/java/com/kawomi/githugandroid/levels/AdvancedLevels.kt
app/src/main/java/com/kawomi/githugandroid/levels/CoreLevels.kt
app/src/main/java/com/kawomi/githugandroid/levels/LevelCatalog.kt
This commit is contained in:
Joe Tretter
2026-04-24 12:17:47 -05:00
parent 54e7e91339
commit 01765a0892
8 changed files with 161 additions and 37 deletions

View File

@@ -10,7 +10,7 @@ This repository contains a starter Android app with:
- CLI-first gameplay shell
- Optional hideable visualization panel
- A native-Git runtime scaffold with in-memory fallback while no bundled Git binary is present
- First playable GitHug-inspired levels: `init`, `add`, and `commit`
- Full official Githug level catalog, split into dedicated Kotlin level files
## Product direction
@@ -96,7 +96,7 @@ Planned native Git packaging path:
## Next steps toward full GitHug parity
- Replace the fallback in-memory Git engine with a packaged native Git binary
- Expand real repository-backed level validation beyond `init`, `add`, and `commit`
- Port all original levels and hints into structured content files
- Tighten advanced real repository-backed validation across the full Githug catalog
- Continue refining per-level setup fidelity for the remaining advanced scenarios
- Add richer validation rules and per-level explanations
- Add onboarding, accessibility polish, icons, tests, and Play Store assets

View File

@@ -11,8 +11,8 @@ android {
applicationId = "com.kawomi.githugandroid"
minSdk = 26
targetSdk = 34
versionCode = 59
versionName = "0.1.58"
versionCode = 61
versionName = "0.1.60"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true

View File

@@ -26,6 +26,8 @@ data class RepoState(
val headBranch: String = "master",
val branches: Map<String, Int> = emptyMap(),
val currentDir: String = ".",
val tags: List<String> = emptyList(),
val remotes: Map<String, String> = emptyMap(),
)
data class Level(
@@ -34,39 +36,11 @@ data class Level(
val description: String,
val hints: List<String>,
val commandSuggestions: List<String>,
val validator: (RepoState) -> Boolean,
val validator: (RepoState, String) -> Boolean,
val setup: () -> RepoState,
)
fun sampleLevels(): List<Level> = listOf(
Level(
id = "init",
title = "Init",
description = "A new directory, git_hug, has been created. Initialize an empty repository in it.",
hints = listOf("Use git init to create a new repository.", "Try `git init` in the command area."),
commandSuggestions = listOf("git init", "git status"),
validator = { it.initialized },
setup = { RepoState() },
),
Level(
id = "add",
title = "Add",
description = "There is a file in your folder called README; add it to your staging area.",
hints = listOf("You want to stage README.", "Use `git add README`."),
commandSuggestions = listOf("git status", "git add README", "ls"),
validator = { repo -> repo.files.any { it.name == "README" && it.staged } },
setup = { RepoState(initialized = true, files = listOf(GitFile("README")), branches = mapOf("master" to 0)) },
),
Level(
id = "commit",
title = "Commit",
description = "The README file has been added to your staging area, now commit it.",
hints = listOf("You must include a message when you commit.", "Use `git commit -m \"message\"`."),
commandSuggestions = listOf("git status", "git commit -m \"Initial commit\"", "git log"),
validator = { repo -> repo.commits.isNotEmpty() },
setup = { RepoState(initialized = true, files = listOf(GitFile("README", staged = true)), branches = mapOf("master" to 0)) },
),
)
fun sampleLevels(): List<Level> = allGithugLevels()
val RepoStateSaver = listSaver<RepoState, Any>(
save = { state ->
@@ -77,6 +51,8 @@ val RepoStateSaver = listSaver<RepoState, Any>(
state.commits.flatMap { listOf(it.id, it.message) },
state.branches.flatMap { listOf(it.key, it.value.toString()) },
state.currentDir,
state.tags,
state.remotes.flatMap { listOf(it.key, it.value) },
)
},
restore = { saved ->
@@ -85,6 +61,8 @@ val RepoStateSaver = listSaver<RepoState, Any>(
val fileParts = saved[2] as List<*>
val commitParts = saved[3] as List<*>
val branchParts = saved[4] as List<*>
val tags = saved[6] as List<*>
val remoteParts = saved[7] as List<*>
RepoState(
initialized = initialized,
headBranch = headBranch,
@@ -101,6 +79,8 @@ val RepoStateSaver = listSaver<RepoState, Any>(
},
branches = branchParts.chunked(2).associate { (it[0] as String) to (it[1] as String).toInt() },
currentDir = saved[5] as String,
tags = tags.filterIsInstance<String>(),
remotes = remoteParts.chunked(2).associate { (it[0] as String) to (it[1] as String) },
)
}
)

View File

@@ -219,7 +219,7 @@ fun GitHugApp() {
historyDraft = ""
val (newRepo, lines) = runtime.execute(currentLevel, repo, raw)
val solvedAfterCommand = currentLevel.validator(newRepo)
val solvedAfterCommand = currentLevel.validator(newRepo, raw)
val wasAlreadyCompleted = currentLevel.id in completedLevels
val newOutput = buildList {
addAll(output)
@@ -236,7 +236,7 @@ fun GitHugApp() {
loadLevel(currentLevelIndex + 1)
} else {
repo = newRepo
output = listOf("🏁 All available MVP levels completed.")
output = listOf("🏁 All Githug levels completed.")
clearCommandInput()
suppressedImeEcho = null
}

View File

@@ -177,6 +177,8 @@ class GitRepositoryRuntime(private val context: Context) {
val logResult = runGit(nativeGit, sandbox, listOf("log", "--pretty=format:%h\t%s"))
val branchResult = runGit(nativeGit, sandbox, listOf("branch", "--list"))
val tagResult = runGit(nativeGit, sandbox, listOf("tag", "--list"))
val remoteResult = runGit(nativeGit, sandbox, listOf("remote", "-v"))
val headResult = runGit(nativeGit, sandbox, listOf("branch", "--show-current"))
val commits = if (logResult.exitCode == 0) {
logResult.outputLines.filter { it.isNotBlank() }.mapNotNull { line ->
@@ -201,6 +203,11 @@ class GitRepositoryRuntime(private val context: Context) {
commits = commits,
headBranch = headResult.outputLines.firstOrNull()?.ifBlank { null } ?: "master",
branches = branchResult.outputLines.map { it.removePrefix("*").trim() }.filter { it.isNotBlank() }.associateWith { 0 },
tags = tagResult.outputLines.filter { it.isNotBlank() },
remotes = remoteResult.outputLines.mapNotNull { line ->
val parts = line.trim().split(Regex("\\s+"))
if (parts.size >= 2) parts[0] to parts[1] else null
}.toMap(),
)
}

View File

@@ -0,0 +1,53 @@
package com.kawomi.githugandroid
internal fun advancedLevels(): List<Level> = listOf(
level("clone", description = "Clone the repository at https://github.com/Gazler/cloneme.", hints = listOf("You should have a look at this site: https://github.com/Gazler/cloneme."), setup = { RepoState() }, validator = commandAnswer("git clone https://github.com/Gazler/cloneme")),
level("clone_to_folder", description = "Clone the repository at https://github.com/Gazler/cloneme into the folder `my_cloned_repo`.", hints = listOf("This is like the last level, `git clone` has an optional argument."), setup = { RepoState() }, validator = commandAnswer("git clone https://github.com/Gazler/cloneme my_cloned_repo")),
level("ignore", description = "The text editor 'vim' creates files ending in `.swp` (swap files) for all files that are currently open. We don't want them creeping into the repository. Make this repository ignore those swap files which are ending in `.swp`.", hints = listOf("You may have noticed there is a file named `.gitignore` in the repository."), setup = { RepoState(initialized = true, files = listOf(GitFile(".gitignore", tracked = true)), branches = mapOf("master" to 0)) }, validator = repoPredicate { it.files.find { f -> f.name == ".gitignore" }?.content?.contains("*.swp") == true }),
level("include", description = "Notice a few files with the '.a' extension. We want git to ignore all the files except the 'lib.a' file.", hints = listOf("Using `git help ignore`, read about the optional prefix to negate a pattern."), setup = { RepoState(initialized = true, files = listOf(GitFile(".gitignore", tracked = true), GitFile("lib.a"), GitFile("main.a")), branches = mapOf("master" to 0)) }, validator = repoPredicate { it.files.find { f -> f.name == ".gitignore" }?.content?.let { c -> "*.a" in c && "!lib.a" in c } == true }),
level("status", description = "Among the files in this repository, which of them is untracked?", hints = listOf("You are looking for a command to identify the status of the repository."), setup = { RepoState(initialized = true, files = listOf(GitFile("database.yml"), GitFile("README", tracked = true)), branches = mapOf("master" to 0)) }, validator = commandAnswer("database.yml")),
level("number_of_files_committed", description = "There are some files in this repository; how many of them are staged for a commit?", hints = listOf("You are looking for a command to identify the status of the repository (resembles a Linux command)."), setup = { RepoState(initialized = true, files = listOf(GitFile("rubyfile1.rb", staged = true), GitFile("rubyfile4.rb", staged = true, tracked = true), GitFile("rubyfile5.rb", tracked = true), GitFile("rubyfile6.rb"), GitFile("rubyfile7.rb")), branches = mapOf("master" to 1)) }, validator = commandAnswer("2")),
level("rm", description = "A file has been removed from the working tree, but not from the repository. Identify this file and remove it.", hints = emptyList(), setup = { RepoState(initialized = true, commits = listOf(CommitNode("0000001", "Added a temp file")), branches = mapOf("master" to 1)) }, validator = { _, command -> command.contains("deleteme.rb") }),
level("rm_cached", description = "A file has accidentally been added to your staging area. Identify and remove it from the staging area. *NOTE* Do not remove the file from the file system, only from git.", hints = listOf("You may need to use more than one command to complete this."), setup = { RepoState(initialized = true, files = listOf(GitFile("deleteme.rb", staged = true), GitFile(".gitignore", staged = true)), branches = mapOf("master" to 0)) }, validator = { repo, _ -> repo.files.any { it.name == "deleteme.rb" && !it.staged } }),
level("stash", description = "You've made some changes and want to work on them later. You should save them, but don't commit them.", hints = listOf("It's like stashing. Try finding an appropriate git command."), setup = { RepoState(initialized = true, files = listOf(GitFile("lyrics.txt", tracked = true)), branches = mapOf("master" to 1)) }, validator = { _, command -> command.startsWith("git stash") }),
level("rename", description = "We have a file called `oldfile.txt`. We want to rename it to `newfile.txt` and stage this change.", hints = listOf("Take a look at `git mv`."), setup = { RepoState(initialized = true, files = listOf(GitFile("oldfile.txt", tracked = true)), commits = listOf(CommitNode("0000001", "Commited oldfile.txt")), branches = mapOf("master" to 1)) }, validator = { repo, _ -> repo.files.any { it.name == "newfile.txt" } }),
level("restructure", description = "You added some files to your repository, but now realize that your project needs to be restructured. Make a new folder named `src` and use Git move all of the .html files into this folder.", hints = listOf("You'll have to use mkdir, and `git mv`."), setup = { RepoState(initialized = true, files = listOf(GitFile("about.html", tracked = true), GitFile("contact.html", tracked = true), GitFile("index.html", tracked = true)), commits = listOf(CommitNode("0000001", "adding web content.")), branches = mapOf("master" to 1)) }, validator = repoPredicate { listOf("src/about.html", "src/contact.html", "src/index.html").all { target -> it.files.any { f -> f.name == target } } }),
level("log", description = "Identify the hash of the latest commit.", hints = listOf("You need to investigate the logs."), setup = { RepoState(initialized = true, commits = listOf(CommitNode("0000001", "THIS IS THE COMMIT YOU ARE LOOKING FOR!")), branches = mapOf("master" to 1)) }, validator = commandAnswer("0000001")),
level("push_tags", description = "A tag in the local repository isn't pushed into remote repository. Push it now.", hints = listOf("Take a look at `--tags` flag of `git push`"), setup = { RepoState(initialized = true, tags = listOf("tag_to_be_pushed"), branches = mapOf("master" to 2), remotes = mapOf("origin" to "remote")) }, validator = { _, command -> command.contains("push") && command.contains("--tags") }),
level("commit_amend", description = "The `README` file has been committed, but it looks like the file `forgotten_file.rb` was missing from the commit. Add the file and amend your previous commit to include it.", hints = listOf("Running `git commit --help` will display the man page and possible flags."), setup = { RepoState(initialized = true, files = listOf(GitFile("README", tracked = true), GitFile("forgotten_file.rb")), commits = listOf(CommitNode("0000001", "Initial commit")), branches = mapOf("master" to 1)) }, validator = { repo, command -> repo.files.any { it.name == "forgotten_file.rb" && it.tracked } && "--amend" in command }),
level("commit_in_future", description = "Commit your changes with the future date (e.g. tomorrow).", hints = listOf("Build a time format, and commit your code using --date parameter."), setup = { RepoState(initialized = true, files = listOf(GitFile("README", staged = true)), branches = mapOf("master" to 0)) }, validator = { repo, command -> repo.commits.isNotEmpty() && "--date" in command }),
level("reset", description = "There are two files to be committed. The goal was to add each file as a separate commit, however both were added by accident. Unstage the file `to_commit_second.rb` using the reset command (don't commit anything).", hints = listOf("git status will tell you the command you need to run."), 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)) }, validator = { repo, _ -> repo.files.any { it.name == "to_commit_first.rb" && it.staged } && repo.files.any { it.name == "to_commit_second.rb" && !it.staged } }),
level("reset_soft", description = "You committed too soon. Now you want to undo the last commit, while keeping the index.", hints = listOf("What are some options you can use with `git reset`?"), 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)) }, validator = { repo, command -> repo.commits.size <= 1 && repo.files.any { it.name == "newfile.rb" && it.staged } && command.contains("--soft") }),
level("checkout_file", description = "A file has been modified, but you don't want to keep the modification. Checkout the `config.rb` file from the last commit.", hints = listOf("You will need to do some research on the checkout command for this one."), 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)) }, validator = { repo, _ -> repo.files.find { it.name == "config.rb" }?.content?.contains("don't want to keep") == false }),
level("remote", description = "This project has a remote repository. Identify it.", hints = listOf("You are looking for a remote. You can run `git` for a list of commands."), setup = { RepoState(initialized = true, remotes = mapOf("my_remote_repo" to "https://github.com/Gazler/githug"), branches = mapOf("master" to 0)) }, validator = commandAnswer("my_remote_repo")),
level("remote_url", description = "The remote repositories have a url associated to them. Please enter the url of remote_location.", hints = listOf("You can run `git remote --help` for the man pages."), setup = { RepoState(initialized = true, remotes = mapOf("my_remote_repo" to "https://github.com/Gazler/githug", "remote_location" to "https://github.com/githug/not_a_repo"), branches = mapOf("master" to 0)) }, validator = commandAnswer("https://github.com/githug/not_a_repo")),
level("pull", description = "You need to pull changes from your origin repository.", hints = listOf("Check out the remote repositories and research `git pull`."), setup = { RepoState(initialized = true, remotes = mapOf("origin" to "https://github.com/pull-this/thing-to-pull"), branches = mapOf("master" to 1)) }, validator = { _, command -> command.startsWith("git pull") }),
level("remote_add", description = "Add a remote repository called `origin` with the url https://github.com/githug/githug", hints = listOf("You can run `git remote --help` for the man pages."), setup = { RepoState(initialized = true, branches = mapOf("master" to 0)) }, validator = repoPredicate { it.remotes["origin"] == "https://github.com/githug/githug" }),
level("push", 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`."), setup = { RepoState(initialized = true, remotes = mapOf("origin" to "remote"), branches = mapOf("master" to 3)) }, validator = { _, command -> command.startsWith("git push") }),
level("diff", description = "Since your last commit, file `app.rb` was modified. Find out which line has changed.", hints = listOf("You are looking for the difference since your last commit."), setup = { RepoState(initialized = true, files = listOf(GitFile("app.rb", tracked = true)), branches = mapOf("master" to 1)) }, validator = commandAnswer("26")),
level("blame", description = "Identify who put a password inside the file `config.rb`.", hints = listOf("You want to research the `git blame` command."), setup = { RepoState(initialized = true, files = listOf(GitFile("config.rb", tracked = true)), branches = mapOf("master" to 1)) }, validator = { _, command -> command.isNotBlank() && !command.startsWith("git") }),
level("checkout_tag", description = "You need to fix a bug in the version 1.2 of your app. Checkout the tag `v1.2`.", hints = listOf("There's no big difference between checking out a branch and checking out a tag."), 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)) }, validator = { _, command -> command.contains("checkout") && command.contains("v1.2") }),
level("checkout_tag_over_branch", description = "You need to fix a bug in the version 1.2 of your app. Checkout the tag `v1.2` (Note: There is also a branch named `v1.2`).", hints = listOf("You should think about specifying you're after the tag named `v1.2` (think `tags/`)."), setup = { RepoState(initialized = true, tags = listOf("v1.0", "v1.2", "v1.5"), branches = mapOf("master" to 5, "v1.2" to 6)) }, validator = { _, command -> "tags/v1.2" in command || command.trim() == "git checkout refs/tags/v1.2" }),
level("branch_at", description = "You forgot to branch at the previous commit and made a commit on top of it. Create the branch test_branch at the commit before the last.", hints = listOf("Just like creating a branch, but you have to pass an extra argument."), setup = { RepoState(initialized = true, commits = listOf(CommitNode("1", "Adding file1"), CommitNode("2", "Updating file1"), CommitNode("3", "Updating file1 again")), branches = mapOf("master" to 3)) }, validator = repoPredicate { "test_branch" in it.branches }),
level("delete_branch", description = "You have created too many branches for your project. There is an old branch in your repo called 'delete_me', you should delete it.", hints = listOf("Running 'git --help branch' will give you a list of branch commands."), setup = { RepoState(initialized = true, branches = mapOf("master" to 1, "delete_me" to 1)) }, validator = repoPredicate { "delete_me" !in it.branches }),
level("push_branch", description = "You've made some changes to a local branch and want to share it, but aren't yet ready to merge it with the 'master' branch. Push only 'test_branch' to the remote repository", hints = listOf("Investigate the options in `git push` using `git push --help`"), setup = { RepoState(initialized = true, branches = mapOf("master" to 2, "other_branch" to 3, "test_branch" to 4), remotes = mapOf("origin" to "remote")) }, validator = { _, command -> command.startsWith("git push") && command.contains("test_branch") }),
level("merge", description = "We have a file in the branch 'feature'. Let's merge it with the master branch.", hints = listOf("You want to research the `git merge` command."), setup = { RepoState(initialized = true, files = listOf(GitFile("file1", tracked = true)), branches = mapOf("master" to 1, "feature" to 2)) }, validator = repoPredicate { it.files.any { f -> f.name == "file2" } }),
level("fetch", description = "Looks like a new branch was pushed into our remote repository. Get the changes without merging them with the local repository", hints = listOf("Look up the 'git fetch' command"), setup = { RepoState(initialized = true, branches = mapOf("master" to 1), remotes = mapOf("origin" to "remote")) }, validator = { _, command -> command.startsWith("git fetch") }),
level("rebase", description = "We are using a git rebase workflow and the feature branch is ready to go into master. Let's rebase the feature branch onto our master branch.", hints = listOf("You want to research the `git rebase` command"), setup = { RepoState(initialized = true, branches = mapOf("master" to 2, "feature" to 3)) }, validator = { _, command -> command.startsWith("git rebase") }),
level("rebase_onto", description = "You have created your branch from `wrong_branch` and already made some commits, and you realise that you needed to create your branch from `master`. Rebase your commits onto `master` branch so that you don't have `wrong_branch` commits.", hints = listOf("You want to research the `git rebase` commands `--onto` argument"), setup = { RepoState(initialized = true, branches = mapOf("master" to 1, "wrong_branch" to 2, "readme-update" to 4)) }, validator = { _, command -> command.startsWith("git rebase") && command.contains("--onto") }),
level("repack", description = "Optimise how your repository is packaged ensuring that redundant packs are removed.", hints = listOf("You want to research the `git repack` command."), setup = { RepoState(initialized = true, files = listOf(GitFile("foo", tracked = true)), commits = listOf(CommitNode("1", "Added foo")), branches = mapOf("master" to 1)) }, validator = { _, command -> command.startsWith("git repack") }),
level("cherry-pick", description = "Your new feature isn't worth the time and you're going to delete it. But it has one commit that fills in `README` file, and you want this commit to be on the master as well.", hints = listOf("Sneak a peek at the `git help cherry-pick` command."), setup = { RepoState(initialized = true, branches = mapOf("master" to 2, "feature" to 3)) }, validator = { _, command -> command.startsWith("git cherry-pick") }),
level("grep", description = "Your project's deadline approaches, you should evaluate how many TODOs are left in your code", hints = listOf("You want to research the `git grep` command."), setup = { RepoState(initialized = true, files = listOf(GitFile("app.rb", "# TODO\n# TODO\n# TODO\n# TODO", tracked = true)), branches = mapOf("master" to 1)) }, validator = commandAnswer("4")),
level("rename_commit", description = "Correct the typo in the message of your first (non-root) commit.", hints = listOf("Take a look the `-i` flag of the rebase command."), setup = { RepoState(initialized = true, commits = listOf(CommitNode("1", "Initial commit"), CommitNode("2", "First coommit"), CommitNode("3", "Second commit")), branches = mapOf("master" to 3)) }, validator = { _, command -> command.startsWith("git rebase -i") || command.contains("First commit") }),
level("squash", description = "You have committed several times but would like all those changes to be one commit.", hints = listOf("Take a look at the `-i` flag of the rebase command."), 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)) }, validator = repoPredicate { it.commits.size <= 2 }),
level("merge_squash", description = "Merge all commits from the long-feature-branch as a single commit.", hints = listOf("Take a look at the `--squash` option of the merge command. Don't forget to commit the merge!"), setup = { RepoState(initialized = true, branches = mapOf("master" to 2, "long-feature-branch" to 4)) }, validator = { _, command -> command.contains("merge") && command.contains("--squash") }),
level("reorder", description = "You have committed several times but in the wrong order. Please reorder your commits.", hints = listOf("Take a look the `-i` flag of the rebase command."), 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)) }, validator = { _, command -> command.startsWith("git rebase -i") }),
level("bisect", description = "A bug was introduced somewhere along the way. You know that running `ruby prog.rb 5` should output 15. You can also run `make test`. What are the first 7 chars of the hash of the commit (the abbreviated hash) that introduced the bug?", hints = emptyList(), setup = { RepoState(initialized = true, branches = mapOf("master" to 7)) }, validator = commandAnswer("18ed2ac")),
level("stage_lines", description = "You've made changes within a single file that belong to two different features, but neither of the changes are yet staged. Stage only the changes belonging to the first feature.", hints = listOf("Read about the flags which can be passed to the `add` command."), setup = { RepoState(initialized = true, files = listOf(GitFile("feature.rb", "this is the class of my feature\nThis change belongs to the first feature\nThis change belongs to the second feature", tracked = true)), branches = mapOf("master" to 1)) }, validator = { _, command -> command.startsWith("git add") && ("-p" in command || "--patch" in command) }),
level("find_old_branch", description = "You have been working on a branch but got distracted by a major issue. Switch back to that branch even though you forgot the name of it.", hints = listOf("Ever played with the `git reflog` command?"), setup = { RepoState(initialized = true, branches = mapOf("master" to 1, "solve_world_hunger" to 2)) }, validator = repoPredicate { it.headBranch == "solve_world_hunger" }),
level("revert", description = "You have committed several times but want to undo the middle commit. All commits have been pushed, so you can't change existing history.", hints = listOf("Try the revert command."), setup = { RepoState(initialized = true, commits = listOf(CommitNode("1", "First commit"), CommitNode("2", "Bad commit"), CommitNode("3", "Second commit")), branches = mapOf("master" to 3)) }, validator = { repo, command -> repo.commits.any { it.message.contains("Revert") } || command.startsWith("git revert") }),
level("restore", description = "You decided to delete your latest commit by running `git reset --hard HEAD^` (not a smart thing to do). Now you changed your mind and want that commit back. Restore the deleted commit.", hints = listOf("The commit is still floating around somewhere. Have you checked out `git reflog`?"), 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)) }, validator = repoPredicate { it.files.any { f -> f.name == "file3" } }),
level("conflict", description = "You need to merge mybranch into the current branch (master). But there may be some incorrect changes in mybranch which may cause conflicts. Solve any merge-conflicts you come across and finish the merge.", hints = emptyList(), setup = { RepoState(initialized = true, files = listOf(GitFile("poem.txt", tracked = true)), branches = mapOf("master" to 2, "mybranch" to 2)) }, validator = { _, command -> command.startsWith("git merge") || command.startsWith("git commit") }),
level("submodule", description = "You want to include the files from the following repo: `https://github.com/jackmaney/githug-include-me` into the folder `./githug-include-me`. Do this without manually cloning the repo or copying the files from the repo into this repo.", hints = listOf("Take a look at `git submodule`."), setup = { RepoState(initialized = true, branches = mapOf("master" to 0)) }, validator = { _, command -> command.startsWith("git submodule add") }),
level("contribute", description = "This is the final level, the goal is to contribute to this repository by making a pull request on GitHub. Please note that this level is designed to encourage you to add a valid contribution to Githug, not testing your ability to create a pull request. Contributions that are likely to be accepted are levels, bug fixes and improved documentation.", hints = listOf("Forking the repository would be a good start!"), setup = { RepoState() }, validator = { _, command -> command.isNotBlank() }),
)

View File

@@ -0,0 +1,64 @@
package com.kawomi.githugandroid
internal fun coreLevels(): List<Level> = listOf(
level(
id = "init",
title = "Init",
description = "A new directory, `git_hug`, has been created; initialize an empty repository in it.",
hints = listOf("You can type `git --help` or `git` in your shell to get a list of available git commands."),
commandSuggestions = listOf("git init", "git status"),
setup = { RepoState() },
validator = repoPredicate { it.initialized },
),
level(
id = "config",
title = "Config",
description = "Set up your git name and email; this is important so that your commits can be identified.",
hints = listOf("Use `git config user.name ...` and `git config user.email ...`."),
commandSuggestions = listOf("git config user.name GitHug", "git config user.email githug@example.com"),
setup = { RepoState(initialized = true, branches = mapOf("master" to 0)) },
validator = repoPredicate { it.initialized },
),
level(
id = "add",
title = "Add",
description = "There is a file in your folder called `README`; add it to your staging area.\nNote: Each level starts with a new repo. Don't look for files of the previous one.",
hints = listOf("You can type `git` in your shell to get a list of available git commands."),
commandSuggestions = listOf("git add README", "git status"),
setup = { RepoState(initialized = true, files = listOf(GitFile("README")), branches = mapOf("master" to 0)) },
validator = repoPredicate { repo -> repo.files.any { it.name == "README" && it.staged } },
),
level(
id = "commit",
title = "Commit",
description = "The `README` file has been added to your staging area, now commit it.",
hints = listOf("You must include a message when you commit."),
commandSuggestions = listOf("git commit -m \"Initial commit\"", "git log"),
setup = { RepoState(initialized = true, files = listOf(GitFile("README", staged = true)), branches = mapOf("master" to 0)) },
validator = repoPredicate { it.commits.isNotEmpty() },
),
level(
id = "branch",
description = "To work on a piece of code that has the potential to break things, create the branch test_code.",
hints = listOf("`git branch` is what you want to investigate."),
commandSuggestions = listOf("git branch test_code", "git branch"),
setup = { RepoState(initialized = true, files = listOf(GitFile("README", tracked = true)), commits = listOf(CommitNode("0000001", "Initial commit")), branches = mapOf("master" to 1)) },
validator = repoPredicate { "test_code" in it.branches },
),
level(
id = "checkout",
description = "Create and switch to a new branch called my_branch. You will need to create a branch like you did in the previous level.",
hints = listOf("Try looking up `git checkout` and `git branch`."),
commandSuggestions = listOf("git checkout -b my_branch", "git branch"),
setup = { RepoState(initialized = true, files = listOf(GitFile("README", tracked = true)), commits = listOf(CommitNode("0000001", "initial commit")), branches = mapOf("master" to 1)) },
validator = repoPredicate { it.headBranch == "my_branch" },
),
level(
id = "tag",
description = "We have a git repo and we want to tag the current commit with `new_tag`.",
hints = listOf("Take a look at `git tag`."),
commandSuggestions = listOf("git tag new_tag", "git tag"),
setup = { RepoState(initialized = true, files = listOf(GitFile("somefile.txt", tracked = true)), commits = listOf(CommitNode("0000001", "Added some file to the repo")), branches = mapOf("master" to 1)) },
validator = repoPredicate { "new_tag" in it.tags },
),
)

View File

@@ -0,0 +1,20 @@
package com.kawomi.githugandroid
fun allGithugLevels(): List<Level> = coreLevels() + advancedLevels()
internal fun level(
id: String,
title: String = id.replace('-', ' ').replace('_', ' ').replaceFirstChar { it.uppercase() },
description: String,
hints: List<String>,
commandSuggestions: List<String> = listOf("git status", "git log", "git help"),
setup: () -> RepoState,
validator: (RepoState, String) -> Boolean,
): Level = Level(id, title, description, hints, commandSuggestions, validator, setup)
internal fun commandAnswer(vararg answers: String): (RepoState, String) -> Boolean = { _, command ->
val normalized = command.trim()
answers.any { it.equals(normalized, ignoreCase = true) }
}
internal fun repoPredicate(block: (RepoState) -> Boolean): (RepoState, String) -> Boolean = { repo, _ -> block(repo) }