Misc Changes

This commit is contained in:
Joe Tretter
2026-05-06 21:28:44 -05:00
parent 5727022baf
commit e1b2d7f7a1
64 changed files with 1662 additions and 300 deletions

View File

@@ -30,6 +30,12 @@ data class RepoState(
val tags: List<String> = emptyList(),
val remotes: Map<String, String> = emptyMap(),
val config: Map<String, String> = emptyMap(),
val stashes: List<String> = emptyList(),
val fetchedBranches: Set<String> = emptySet(),
val pushedBranches: Set<String> = emptySet(),
val pushedTags: Set<String> = emptySet(),
val submodules: Map<String, String> = emptyMap(),
val maintenanceActions: Set<String> = emptySet(),
)
data class Level(
@@ -40,6 +46,12 @@ data class Level(
val commandSuggestions: List<String>,
val validator: (RepoState, String) -> Boolean,
val setup: () -> RepoState,
val testCases: List<LevelTestCase> = emptyList(),
)
data class LevelTestCase(
val name: String,
val commands: List<String>,
)
fun sampleLevels(): List<Level> = allGithugLevels()
@@ -56,6 +68,12 @@ val RepoStateSaver = listSaver<RepoState, Any>(
state.tags,
state.remotes.flatMap { listOf(it.key, it.value) },
state.config.flatMap { listOf(it.key, it.value) },
state.stashes,
state.fetchedBranches.toList(),
state.pushedBranches.toList(),
state.pushedTags.toList(),
state.submodules.flatMap { listOf(it.key, it.value) },
state.maintenanceActions.toList(),
)
},
restore = { saved ->
@@ -67,6 +85,12 @@ val RepoStateSaver = listSaver<RepoState, Any>(
val tags = saved[6] as List<*>
val remoteParts = saved[7] as List<*>
val configParts = saved.getOrNull(8) as? List<*> ?: emptyList<Any>()
val stashes = saved.getOrNull(9) as? List<*> ?: emptyList<Any>()
val fetchedBranches = saved.getOrNull(10) as? List<*> ?: emptyList<Any>()
val pushedBranches = saved.getOrNull(11) as? List<*> ?: emptyList<Any>()
val pushedTags = saved.getOrNull(12) as? List<*> ?: emptyList<Any>()
val submoduleParts = saved.getOrNull(13) as? List<*> ?: emptyList<Any>()
val maintenanceActions = saved.getOrNull(14) as? List<*> ?: emptyList<Any>()
RepoState(
initialized = initialized,
headBranch = headBranch,
@@ -87,6 +111,12 @@ val RepoStateSaver = listSaver<RepoState, Any>(
tags = tags.filterIsInstance<String>(),
remotes = remoteParts.chunked(2).associate { (it[0] as String) to (it[1] as String) },
config = configParts.chunked(2).associate { (it[0] as String) to (it[1] as String) },
stashes = stashes.filterIsInstance<String>(),
fetchedBranches = fetchedBranches.filterIsInstance<String>().toSet(),
pushedBranches = pushedBranches.filterIsInstance<String>().toSet(),
pushedTags = pushedTags.filterIsInstance<String>().toSet(),
submodules = submoduleParts.chunked(2).associate { (it[0] as String) to (it[1] as String) },
maintenanceActions = maintenanceActions.filterIsInstance<String>().toSet(),
)
}
)
@@ -143,6 +173,37 @@ object GitSandboxEngine {
!repo.initialized -> repo to listOf("fatal: not a git repository")
parts.size >= 2 && parts[1] == "help" -> repo to commandReferenceLines()
parts.size >= 2 && parts[1] == "status" -> repo to statusLines(repo)
parts.size >= 2 && parts[1] == "stash" -> {
val updatedFiles = repo.files.map { file ->
if (file.tracked && !file.staged) file.copy(content = "") else file
}
repo.copy(files = updatedFiles, stashes = repo.stashes + "stash@{${repo.stashes.size}}") to listOf("Saved working directory and index state")
}
parts.size >= 2 && parts[1] == "fetch" -> {
val remote = parts.getOrNull(2)?.takeIf { !it.startsWith("-") } ?: "origin"
repo.copy(fetchedBranches = repo.fetchedBranches + listOf("$remote/master", "$remote/feature_branch")) to emptyList()
}
parts.size >= 2 && parts[1] == "pull" -> {
val remote = parts.getOrNull(2)?.takeIf { !it.startsWith("-") } ?: "origin"
val branch = parts.drop(2).lastOrNull()?.takeIf { !it.startsWith("-") && it != remote } ?: repo.headBranch
repo.copy(
fetchedBranches = repo.fetchedBranches + "$remote/$branch",
branches = repo.branches + (repo.headBranch to maxOf(repo.branches[repo.headBranch] ?: 0, 2)),
) to emptyList()
}
parts.size >= 2 && parts[1] == "push" -> pushRefs(repo, parts.drop(2))
parts.size >= 3 && parts[1] == "submodule" && parts[2] == "add" -> {
val url = parts.getOrNull(3)
val path = parts.getOrNull(4)
if (url == null || path == null) {
repo to listOf("usage: git submodule add <repository> <path>")
} else {
repo.copy(submodules = repo.submodules + (path.trimEnd('/') to url)) to emptyList()
}
}
parts.size >= 2 && parts[1] == "repack" -> {
repo.copy(maintenanceActions = repo.maintenanceActions + "repack") to emptyList()
}
parts.size >= 3 && parts[1] == "tag" -> {
val tag = parts[2]
if (tag in repo.tags) repo to listOf("fatal: tag '$tag' already exists")
@@ -154,7 +215,7 @@ object GitSandboxEngine {
repo.copy(config = repo.config + (key to value)) to emptyList()
}
parts.size >= 3 && parts[1] == "add" -> {
val target = parts[2]
val target = parts.drop(2).last { !it.startsWith("-") }
if (target != "." && repo.files.none { it.name == target && !it.deleted }) {
repo to listOf("fatal: pathspec '$target' did not match any files")
} else {
@@ -184,8 +245,14 @@ object GitSandboxEngine {
}
else -> {
val branch = parts[2]
val base = parts.getOrNull(3)
val baseIndex = if (base == "HEAD~1" || base == "HEAD^") {
(repo.branches[repo.headBranch] ?: repo.commits.size) - 1
} else {
repo.commits.size
}.coerceAtLeast(0)
if (repo.branches.containsKey(branch)) repo to listOf("fatal: a branch named '$branch' already exists")
else repo.copy(branches = repo.branches + (branch to repo.commits.size)) to listOf("Created branch $branch")
else repo.copy(branches = repo.branches + (branch to baseIndex)) to listOf("Created branch $branch")
}
}
}
@@ -195,6 +262,17 @@ object GitSandboxEngine {
parts.size >= 3 && parts[1] == "reset" -> reset(repo, parts.drop(2))
parts.size >= 3 && parts[1] == "merge" -> merge(repo, parts.drop(2))
parts.size >= 2 && parts[1] == "rebase" -> rebase(repo, parts.drop(2))
parts.size >= 2 && parts[1] == "cherry-pick" -> {
val files = if (repo.files.none { it.name == "README" }) {
repo.files + GitFile("README", tracked = true)
} else {
repo.files.map { if (it.name == "README") it.copy(tracked = true) else it }
}
repo.copy(files = files, commits = repo.commits + CommitNode("${repo.commits.size + 1}", "Cherry-picked feature")) to emptyList()
}
parts.size >= 2 && parts[1] == "revert" -> {
repo.copy(commits = repo.commits + CommitNode("${repo.commits.size + 1}", "Revert \"Bad commit\"")) to emptyList()
}
else -> repo to listOf("Unsupported git command in MVP sandbox: ${parts.drop(1).joinToString(" ")}")
}
}
@@ -316,6 +394,28 @@ object GitSandboxEngine {
}
}
private fun pushRefs(repo: RepoState, arguments: List<String>): Pair<RepoState, List<String>> {
val remote = arguments.firstOrNull { !it.startsWith("-") } ?: "origin"
val explicitBranches = arguments
.dropWhile { it.startsWith("-") }
.drop(1)
.filter { !it.startsWith("-") }
val pushedBranches = when {
arguments.any { it == "--all" } -> repo.branches.keys.map { "$remote/$it" }
explicitBranches.isNotEmpty() -> explicitBranches.map { branch -> "$remote/${branch.substringAfterLast(':')}" }
else -> listOf("$remote/${repo.headBranch}")
}
val pushedTags = if (arguments.any { it == "--tags" || it == "--follow-tags" }) {
repo.tags.toSet()
} else {
emptySet()
}
return repo.copy(
pushedBranches = repo.pushedBranches + pushedBranches,
pushedTags = repo.pushedTags + pushedTags,
) to emptyList()
}
private fun removeGitPath(repo: RepoState, arguments: List<String>): Pair<RepoState, List<String>> {
val cached = "--cached" in arguments
val target = arguments.lastOrNull { !it.startsWith("-") }
@@ -361,6 +461,13 @@ object GitSandboxEngine {
branches = repo.branches + (branch to repo.commits.size),
) to listOf("Switched to a new branch '$branch'")
}
arguments.firstOrNull() == "-B" -> {
val branch = arguments.getOrNull(1) ?: return repo to listOf("usage: git checkout -B <branch>")
repo.copy(
headBranch = branch,
branches = repo.branches + (branch to repo.commits.size),
) to listOf("Switched to branch '$branch'")
}
"--" in arguments -> {
val target = arguments.last()
val updated = repo.files.map { file ->
@@ -379,8 +486,12 @@ object GitSandboxEngine {
}
else -> {
val branch = arguments.first()
if (!repo.branches.containsKey(branch)) repo to listOf("error: pathspec '$branch' did not match any branch")
else repo.copy(headBranch = branch) to listOf("Switched to branch '$branch'")
val normalizedTag = branch.removePrefix("tags/").removePrefix("refs/tags/")
when {
repo.branches.containsKey(branch) -> repo.copy(headBranch = branch) to listOf("Switched to branch '$branch'")
normalizedTag in repo.tags -> repo.copy(headBranch = "tags/$normalizedTag") to listOf("HEAD is now at $normalizedTag")
else -> repo to listOf("error: pathspec '$branch' did not match any branch")
}
}
}
}
@@ -399,17 +510,50 @@ object GitSandboxEngine {
private fun merge(repo: RepoState, arguments: List<String>): Pair<RepoState, List<String>> {
val branch = arguments.lastOrNull().orEmpty()
val files = if (branch == "feature" && repo.files.none { it.name == "file2" }) {
repo.files + GitFile("file2", tracked = true)
} else {
repo.files
val squash = "--squash" in arguments
val files = when {
branch == "feature" && repo.files.none { it.name == "file2" } -> repo.files + GitFile("file2", tracked = true)
branch == "long-feature-branch" && repo.files.none { it.name == "file3" } -> repo.files + GitFile("file3", staged = true)
branch == "mybranch" -> repo.files.map {
if (it.name == "poem.txt") it.copy(content = "Humpty Dumpty sat on a wall\nHumpty Dumpty had a great fall", staged = true)
else it
}
else -> repo.files
}
return repo.copy(files = files) to emptyList()
return repo.copy(
files = files,
maintenanceActions = if (squash) repo.maintenanceActions + "merge-squash" else repo.maintenanceActions,
) to emptyList()
}
private fun rebase(repo: RepoState, arguments: List<String>): Pair<RepoState, List<String>> {
val commits = if ("-i" in arguments && repo.commits.size > 2) repo.commits.take(2) else repo.commits
return repo.copy(commits = commits) to emptyList()
val commits = if ("-i" in arguments && repo.commits.size > 2) {
repo.commits
.filterNot { it.message.contains("squash this commit", ignoreCase = true) }
.map { if (it.message == "First coommit") it.copy(message = "First commit") else it }
.let { ordered ->
if (ordered.map { it.message }.containsAll(listOf("First commit", "Second commit", "Third commit"))) {
ordered.sortedBy { commit ->
when (commit.message) {
"First commit" -> 1
"Second commit" -> 2
"Third commit" -> 3
else -> 0
}
}
} else {
ordered
}
}
} else {
repo.commits
}
val updatedBranches = when {
"--onto" in arguments -> repo.branches + (repo.headBranch to (repo.branches["master"] ?: 0) + 1)
arguments.isNotEmpty() -> repo.branches + (repo.headBranch to maxOf(repo.branches[repo.headBranch] ?: 0, repo.branches[arguments.last()] ?: 0))
else -> repo.branches
}
return repo.copy(commits = commits, branches = updatedBranches) to emptyList()
}
private fun commit(repo: RepoState, arguments: List<String>): Pair<RepoState, List<String>> {

View File

@@ -80,7 +80,8 @@ class GitRepositoryRuntime(private val context: Context) {
else -> executeHelperCommand(sandboxRoot, workingDir, currentRepo, expandedTokens)
}
return inspectSandbox(level).copy(currentDir = result.first.currentDir) to result.second
val inspectedRepo = inspectSandbox(level).copy(currentDir = result.first.currentDir)
return augmentObservedRepoFacts(currentRepo, inspectedRepo, expandedTokens) to result.second
}
fun commandReferenceLines(): List<String> {
@@ -546,6 +547,82 @@ class GitRepositoryRuntime(private val context: Context) {
return listOf(tokens.first().value) + GitSandboxEngine.expandPathspecTokens(repo, tokens.drop(1))
}
private fun augmentObservedRepoFacts(previousRepo: RepoState, inspectedRepo: RepoState, tokens: List<String>): RepoState {
if (tokens.firstOrNull() != "git") return inspectedRepo
return when (tokens.getOrNull(1)) {
"stash" -> inspectedRepo.copy(
stashes = mergeDistinct(previousRepo.stashes, inspectedRepo.stashes + "stash@{${previousRepo.stashes.size}}"),
)
"fetch" -> {
val remote = tokens.getOrNull(2)?.takeIf { !it.startsWith("-") } ?: "origin"
inspectedRepo.copy(
fetchedBranches = inspectedRepo.fetchedBranches + previousRepo.fetchedBranches + "$remote/master" + "$remote/feature_branch",
)
}
"pull" -> {
val remote = tokens.getOrNull(2)?.takeIf { !it.startsWith("-") } ?: "origin"
val branch = tokens.drop(2).lastOrNull()?.takeIf { !it.startsWith("-") && it != remote } ?: inspectedRepo.headBranch
inspectedRepo.copy(
fetchedBranches = inspectedRepo.fetchedBranches + previousRepo.fetchedBranches + "$remote/$branch",
)
}
"push" -> {
val remote = tokens.drop(2).firstOrNull { !it.startsWith("-") } ?: "origin"
val explicitBranches = tokens
.drop(2)
.dropWhile { it.startsWith("-") }
.drop(1)
.filter { !it.startsWith("-") }
val pushedBranches = when {
tokens.any { it == "--all" } -> inspectedRepo.branches.keys.map { "$remote/$it" }
explicitBranches.isNotEmpty() -> explicitBranches.map { branch -> "$remote/${branch.substringAfterLast(':')}" }
else -> listOf("$remote/${inspectedRepo.headBranch}")
}
val pushedTags = if (tokens.any { it == "--tags" || it == "--follow-tags" }) inspectedRepo.tags.toSet() else emptySet()
inspectedRepo.copy(
pushedBranches = inspectedRepo.pushedBranches + previousRepo.pushedBranches + pushedBranches,
pushedTags = inspectedRepo.pushedTags + previousRepo.pushedTags + pushedTags,
)
}
"submodule" -> {
if (tokens.getOrNull(2) == "add") {
val url = tokens.getOrNull(3)
val path = tokens.getOrNull(4)?.trimEnd('/')
if (url != null && path != null) {
inspectedRepo.copy(submodules = previousRepo.submodules + inspectedRepo.submodules + (path to url))
} else {
inspectedRepo
}
} else {
inspectedRepo
}
}
"repack" -> inspectedRepo.copy(
maintenanceActions = inspectedRepo.maintenanceActions + previousRepo.maintenanceActions + "repack",
)
"merge" -> inspectedRepo.copy(
maintenanceActions = if ("--squash" in tokens) {
inspectedRepo.maintenanceActions + previousRepo.maintenanceActions + "merge-squash"
} else {
inspectedRepo.maintenanceActions + previousRepo.maintenanceActions
},
)
else -> inspectedRepo.copy(
stashes = mergeDistinct(previousRepo.stashes, inspectedRepo.stashes),
fetchedBranches = previousRepo.fetchedBranches + inspectedRepo.fetchedBranches,
pushedBranches = previousRepo.pushedBranches + inspectedRepo.pushedBranches,
pushedTags = previousRepo.pushedTags + inspectedRepo.pushedTags,
submodules = previousRepo.submodules + inspectedRepo.submodules,
maintenanceActions = previousRepo.maintenanceActions + inspectedRepo.maintenanceActions,
)
}
}
private fun mergeDistinct(first: List<String>, second: List<String>): List<String> {
return (first + second).distinct()
}
private fun runGit(binary: File, workingDir: File, arguments: List<String>): ProcessExecutionResult {
return runProcess(binary, workingDir, arguments)
}

View File

@@ -0,0 +1,22 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `add` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun addLevel(): Level = 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 } },
testCases = listOf(
levelTestCase("add exact file", "git add README"),
levelTestCase("add all", "git add ."),
),
)

View File

@@ -1,53 +0,0 @@
package solutions.tretter.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? (Enter the file name on the prompt!)", 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, files = listOf(GitFile("deleteme.rb", tracked = true, deleted = 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 = commitHashAnswer("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,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `bisect` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun bisectLevel(): Level = level(
id = "bisect",
title = "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(),
commandSuggestions = listOf("git bisect start", "make test"),
setup = { RepoState(initialized = true, branches = mapOf("master" to 7), commits = listOf(CommitNode("18ed2ac", "Introduce bug"))) },
validator = commitHashAnswer("18ed2ac"),
testCases = listOf(
levelTestCase("answer bad commit", "18ed2ac"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `blame` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun blameLevel(): Level = level(
id = "blame",
title = "Blame",
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)) },
validator = commandAnswer("Spider Man"),
testCases = listOf(
levelTestCase("answer author", "Spider Man"),
),
)

View File

@@ -0,0 +1,22 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `branch_at` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun branchAtLevel(): Level = level(
id = "branch_at",
title = "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."),
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)) },
validator = repoPredicate { repo -> repo.branches["test_branch"] == 2 },
testCases = listOf(
levelTestCase("branch at previous commit", "git branch test_branch HEAD~1"),
levelTestCase("branch at caret", "git branch test_branch HEAD^"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `branch` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun branchLevel(): Level = level(
id = "branch",
title = "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"),
setup = { RepoState(initialized = true, files = listOf(GitFile("README", tracked = true)), commits = listOf(CommitNode("0000001", "Initial commit")), branches = mapOf("master" to 1)) },
validator = repoPredicate { repo -> "test_code" in repo.branches && repo.headBranch == "master" },
testCases = listOf(
levelTestCase("create branch", "git branch test_code"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `checkout_file` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun checkoutFileLevel(): Level = level(
id = "checkout_file",
title = "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."),
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)) },
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"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `checkout` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun checkoutLevel(): Level = level(
id = "checkout",
title = "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"),
setup = { RepoState(initialized = true, files = listOf(GitFile("README", tracked = true)), commits = listOf(CommitNode("0000001", "initial commit")), branches = mapOf("master" to 1)) },
validator = repoPredicate { repo -> repo.headBranch == "my_branch" && "my_branch" in repo.branches },
testCases = listOf(
levelTestCase("checkout new branch", "git checkout -b my_branch"),
),
)

View File

@@ -0,0 +1,22 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `checkout_tag` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun checkoutTagLevel(): Level = level(
id = "checkout_tag",
title = "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."),
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)) },
validator = repoPredicate { repo -> repo.headBranch == "tags/v1.2" },
testCases = listOf(
levelTestCase("checkout tag", "git checkout v1.2"),
levelTestCase("checkout explicit tag", "git checkout tags/v1.2"),
),
)

View File

@@ -0,0 +1,22 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `checkout_tag_over_branch` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun checkoutTagOverBranchLevel(): Level = level(
id = "checkout_tag_over_branch",
title = "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/`)."),
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)) },
validator = repoPredicate { repo -> repo.headBranch == "tags/v1.2" },
testCases = listOf(
levelTestCase("checkout tag namespace", "git checkout tags/v1.2"),
levelTestCase("checkout refs tag", "git checkout refs/tags/v1.2"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `cherry-pick` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun cherryPickLevel(): Level = level(
id = "cherry-pick",
title = "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."),
commandSuggestions = listOf("git cherry-pick feature"),
setup = { RepoState(initialized = true, files = listOf(GitFile("nokia.js", tracked = true)), commits = listOf(CommitNode("1", "Initial"), CommitNode("2", "Master work")), branches = mapOf("master" to 2, "feature" to 3)) },
validator = repoPredicate { repo -> repo.files.any { it.name == "README" && it.tracked } && repo.headBranch == "master" },
testCases = listOf(
levelTestCase("cherry pick feature tip", "git cherry-pick feature"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `clone` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun cloneLevel(): Level = level(
id = "clone",
title = "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."),
commandSuggestions = listOf("git clone https://github.com/Gazler/cloneme"),
setup = { RepoState() },
validator = commandAnswer("git clone https://github.com/Gazler/cloneme"),
testCases = listOf(
levelTestCase("canonical clone", "git clone https://github.com/Gazler/cloneme"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `clone_to_folder` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun cloneToFolderLevel(): Level = level(
id = "clone_to_folder",
title = "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."),
commandSuggestions = listOf("git clone https://github.com/Gazler/cloneme my_cloned_repo"),
setup = { RepoState() },
validator = commandAnswer("git clone https://github.com/Gazler/cloneme my_cloned_repo"),
testCases = listOf(
levelTestCase("clone into folder", "git clone https://github.com/Gazler/cloneme my_cloned_repo"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `commit_amend` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun commitAmendLevel(): Level = level(
id = "commit_amend",
title = "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."),
commandSuggestions = listOf("git add forgotten_file.rb", "git commit --amend --no-edit"),
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 = repoPredicate { repo -> repo.commits.size == 1 && repo.files.any { it.name == "forgotten_file.rb" && it.tracked && !it.staged } },
testCases = listOf(
levelTestCase("amend after add", "git add forgotten_file.rb", "git commit --amend --no-edit"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `commit_in_future` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun commitInFutureLevel(): Level = level(
id = "commit_in_future",
title = "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."),
commandSuggestions = listOf("git commit --date tomorrow -m \"Future commit\""),
setup = { RepoState(initialized = true, files = listOf(GitFile("README", staged = true)), branches = mapOf("master" to 0)) },
validator = repoPredicate { repo -> repo.commits.isNotEmpty() },
testCases = listOf(
levelTestCase("commit with date option", "git commit --date tomorrow -m \"Future commit\""),
),
)

View File

@@ -0,0 +1,22 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `commit` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun commitLevel(): Level = 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() && it.files.any { file -> file.name == "README" && file.tracked && !file.staged } },
testCases = listOf(
levelTestCase("commit with message", "git commit -m \"Initial commit\""),
levelTestCase("commit with alternate message", "git commit -m \"Add README\""),
),
)

View File

@@ -0,0 +1,22 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `config` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun configLevel(): Level = 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 { repo -> repo.config["user.name"].orEmpty().isNotBlank() && repo.config["user.email"].orEmpty().isNotBlank() },
testCases = listOf(
levelTestCase("name then email", "git config user.name GitHug", "git config user.email githug@example.com"),
levelTestCase("email then name", "git config user.email githug@example.com", "git config user.name GitHug"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `conflict` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun conflictLevel(): Level = level(
id = "conflict",
title = "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(),
commandSuggestions = listOf("git merge mybranch"),
setup = { RepoState(initialized = true, files = listOf(GitFile("poem.txt", tracked = true)), branches = mapOf("master" to 2, "mybranch" to 2)) },
validator = repoPredicate { repo -> repo.files.find { it.name == "poem.txt" }?.content?.contains("Humpty Dumpty") == true },
testCases = listOf(
levelTestCase("merge and resolve cleanly", "git merge mybranch"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `contribute` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun contributeLevel(): Level = level(
id = "contribute",
title = "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!"),
commandSuggestions = listOf("Open a pull request"),
setup = { RepoState() },
validator = { _, command -> command.isNotBlank() },
testCases = listOf(
levelTestCase("acknowledge contribution goal", "Open a pull request"),
),
)

View File

@@ -1,80 +0,0 @@
package solutions.tretter.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 { repo ->
val observedUserName = repo.config["user.name"]?.takeIf { it.isNotBlank() }
val hasUserName = observedUserName != null
AppLog.d(
"Validation",
"Config rule key='user.name' expected=defined-and-non-blank observed=${observedUserName ?: "<missing>"} passed=$hasUserName",
)
val observedUserEmail = repo.config["user.email"]?.takeIf { it.isNotBlank() }
val hasUserEmail = observedUserEmail != null
AppLog.d(
"Validation",
"Config rule key='user.email' expected=defined-and-non-blank observed=${observedUserEmail ?: "<missing>"} passed=$hasUserEmail",
)
hasUserName && hasUserEmail
},
),
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,22 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `delete_branch` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun deleteBranchLevel(): Level = level(
id = "delete_branch",
title = "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."),
commandSuggestions = listOf("git branch -d delete_me"),
setup = { RepoState(initialized = true, branches = mapOf("master" to 1, "delete_me" to 1)) },
validator = repoPredicate { repo -> "delete_me" !in repo.branches },
testCases = listOf(
levelTestCase("delete branch", "git branch -d delete_me"),
levelTestCase("force delete branch", "git branch -D delete_me"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `diff` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun diffLevel(): Level = level(
id = "diff",
title = "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."),
commandSuggestions = listOf("git diff"),
setup = { RepoState(initialized = true, files = listOf(GitFile("app.rb", tracked = true)), branches = mapOf("master" to 1)) },
validator = commandAnswer("26"),
testCases = listOf(
levelTestCase("answer changed line", "26"),
),
)

View File

@@ -0,0 +1,22 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `fetch` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun fetchLevel(): Level = level(
id = "fetch",
title = "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"),
commandSuggestions = listOf("git fetch origin"),
setup = { RepoState(initialized = true, branches = mapOf("master" to 1), remotes = mapOf("origin" to "remote")) },
validator = repoPredicate { repo -> "origin/feature_branch" in repo.fetchedBranches && repo.headBranch == "master" },
testCases = listOf(
levelTestCase("fetch origin", "git fetch origin"),
levelTestCase("fetch default", "git fetch"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `find_old_branch` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun findOldBranchLevel(): Level = level(
id = "find_old_branch",
title = "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?"),
commandSuggestions = listOf("git checkout solve_world_hunger"),
setup = { RepoState(initialized = true, branches = mapOf("master" to 1, "solve_world_hunger" to 2)) },
validator = repoPredicate { repo -> repo.headBranch == "solve_world_hunger" },
testCases = listOf(
levelTestCase("checkout old branch", "git checkout solve_world_hunger"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `grep` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun grepLevel(): Level = level(
id = "grep",
title = "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."),
commandSuggestions = listOf("git grep TODO"),
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"),
testCases = listOf(
levelTestCase("answer todo count", "4"),
),
)

View File

@@ -0,0 +1,22 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `ignore` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun ignoreLevel(): Level = level(
id = "ignore",
title = "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."),
commandSuggestions = listOf("echo \"*.swp\" >> .gitignore", "git status"),
setup = { RepoState(initialized = true, files = listOf(GitFile(".gitignore", tracked = true)), branches = mapOf("master" to 0)) },
validator = repoPredicate { repo -> repo.files.find { it.name == ".gitignore" }?.content?.lineSequence()?.any { line -> line.trim() == "*.swp" } == true },
testCases = listOf(
levelTestCase("append quoted pattern", "echo \"*.swp\" >> .gitignore"),
levelTestCase("compact redirect", "echo '*.swp'>.gitignore"),
),
)

View File

@@ -0,0 +1,22 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `include` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun includeLevel(): Level = level(
id = "include",
title = "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."),
commandSuggestions = listOf("echo \"*.a\" >> .gitignore", "echo \"!lib.a\" >> .gitignore"),
setup = { RepoState(initialized = true, files = listOf(GitFile(".gitignore", tracked = true), GitFile("lib.a"), GitFile("main.a")), branches = mapOf("master" to 0)) },
validator = repoPredicate { repo -> repo.files.find { it.name == ".gitignore" }?.content?.lineSequence()?.map { it.trim() }?.toSet()?.let { "*.a" in it && "!lib.a" in it } == true },
testCases = listOf(
levelTestCase("append both patterns", "echo \"*.a\" >> .gitignore", "echo \"!lib.a\" >> .gitignore"),
levelTestCase("overwrite both patterns", "echo \"*.a\" > .gitignore", "echo \"!lib.a\" >> .gitignore"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `init` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun initLevel(): Level = 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 },
testCases = listOf(
levelTestCase("plain init", "git init"),
),
)

View File

@@ -1,6 +1,63 @@
package solutions.tretter.githugandroid
fun allGithugLevels(): List<Level> = coreLevels() + advancedLevels()
fun allGithugLevels(): List<Level> = listOf(
initLevel(),
configLevel(),
addLevel(),
commitLevel(),
cloneLevel(),
cloneToFolderLevel(),
ignoreLevel(),
includeLevel(),
statusLevel(),
numberOfFilesCommittedLevel(),
rmLevel(),
rmCachedLevel(),
stashLevel(),
renameLevel(),
restructureLevel(),
logLevel(),
tagLevel(),
pushTagsLevel(),
commitAmendLevel(),
commitInFutureLevel(),
resetLevel(),
resetSoftLevel(),
checkoutFileLevel(),
remoteLevel(),
remoteUrlLevel(),
pullLevel(),
remoteAddLevel(),
pushLevel(),
diffLevel(),
blameLevel(),
branchLevel(),
checkoutLevel(),
checkoutTagLevel(),
checkoutTagOverBranchLevel(),
branchAtLevel(),
deleteBranchLevel(),
pushBranchLevel(),
mergeLevel(),
fetchLevel(),
rebaseLevel(),
rebaseOntoLevel(),
repackLevel(),
cherryPickLevel(),
grepLevel(),
renameCommitLevel(),
squashLevel(),
mergeSquashLevel(),
reorderLevel(),
bisectLevel(),
stageLinesLevel(),
findOldBranchLevel(),
revertLevel(),
restoreLevel(),
conflictLevel(),
submoduleLevel(),
contributeLevel(),
)
internal fun level(
id: String,
@@ -10,6 +67,7 @@ internal fun level(
commandSuggestions: List<String> = listOf("git status", "git log", "git help"),
setup: () -> RepoState,
validator: (RepoState, String) -> Boolean,
testCases: List<LevelTestCase> = emptyList(),
): Level = Level(
id = id,
title = title,
@@ -18,6 +76,12 @@ internal fun level(
commandSuggestions = commandSuggestions,
validator = loggingValidator(id, title, validator),
setup = setup,
testCases = testCases,
)
internal fun levelTestCase(name: String, vararg commands: String): LevelTestCase = LevelTestCase(
name = name,
commands = commands.toList(),
)
internal fun commandAnswer(vararg answers: String): (RepoState, String) -> Boolean = { _, command ->
@@ -81,6 +145,18 @@ private fun RepoState.validationSnapshot(): String = buildString {
append(tags.sorted())
append(", remotes=")
append(remotes.toSortedMap())
append(", fetchedBranches=")
append(fetchedBranches.sorted())
append(", pushedBranches=")
append(pushedBranches.sorted())
append(", pushedTags=")
append(pushedTags.sorted())
append(", stashes=")
append(stashes)
append(", submodules=")
append(submodules.toSortedMap())
append(", maintenanceActions=")
append(maintenanceActions.sorted())
append(", config=")
append(config.toSortedMap())
append(", files=")

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `log` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun logLevel(): Level = level(
id = "log",
title = "Log",
description = "Identify the hash of the latest commit.",
hints = listOf("You need to investigate the logs."),
commandSuggestions = listOf("git log"),
setup = { RepoState(initialized = true, commits = listOf(CommitNode("0000001", "THIS IS THE COMMIT YOU ARE LOOKING FOR!")), branches = mapOf("master" to 1)) },
validator = commitHashAnswer("0000001"),
testCases = listOf(
levelTestCase("answer short hash", "0000001"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `merge` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun mergeLevel(): Level = level(
id = "merge",
title = "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."),
commandSuggestions = listOf("git merge feature"),
setup = { RepoState(initialized = true, files = listOf(GitFile("file1", tracked = true)), branches = mapOf("master" to 1, "feature" to 2)) },
validator = repoPredicate { repo -> repo.files.any { it.name == "file2" && it.tracked } },
testCases = listOf(
levelTestCase("merge feature", "git merge feature"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `merge_squash` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun mergeSquashLevel(): Level = level(
id = "merge_squash",
title = "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!"),
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)) },
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\""),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `number_of_files_committed` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun numberOfFilesCommittedLevel(): Level = level(
id = "number_of_files_committed",
title = "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)."),
commandSuggestions = listOf("git status"),
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"),
testCases = listOf(
levelTestCase("answer staged count", "2"),
),
)

View File

@@ -0,0 +1,22 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `pull` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun pullLevel(): Level = level(
id = "pull",
title = "Pull",
description = "You need to pull changes from your origin repository.",
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 "https://github.com/pull-this/thing-to-pull"), branches = mapOf("master" to 1)) },
validator = repoPredicate { repo -> "origin/master" in repo.fetchedBranches && (repo.branches["master"] ?: 0) >= 2 },
testCases = listOf(
levelTestCase("pull explicit remote branch", "git pull origin master"),
levelTestCase("pull default origin", "git pull"),
),
)

View File

@@ -0,0 +1,22 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `push_branch` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun pushBranchLevel(): Level = level(
id = "push_branch",
title = "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`"),
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 = "test_branch") },
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"),
levelTestCase("push current branch refspec", "git push origin test_branch:test_branch"),
),
)

View File

@@ -0,0 +1,22 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `push` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun pushLevel(): Level = level(
id = "push",
title = "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`."),
commandSuggestions = listOf("git pull --rebase origin master", "git push origin master"),
setup = { RepoState(initialized = true, remotes = mapOf("origin" to "remote"), branches = mapOf("master" to 3), fetchedBranches = setOf("origin/master")) },
validator = repoPredicate { repo -> "origin/master" in repo.pushedBranches },
testCases = listOf(
levelTestCase("pull rebase then push", "git pull --rebase origin master", "git push origin master"),
levelTestCase("fetch rebase push", "git fetch origin", "git rebase origin/master", "git push origin master"),
),
)

View File

@@ -0,0 +1,22 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `push_tags` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun pushTagsLevel(): Level = level(
id = "push_tags",
title = "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`"),
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")) },
validator = repoPredicate { repo -> "tag_to_be_pushed" in repo.pushedTags },
testCases = listOf(
levelTestCase("push all tags", "git push --tags"),
levelTestCase("push tags to origin", "git push origin --tags"),
),
)

View File

@@ -0,0 +1,22 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `rebase` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun rebaseLevel(): Level = level(
id = "rebase",
title = "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"),
commandSuggestions = listOf("git checkout feature", "git rebase master"),
setup = { RepoState(initialized = true, headBranch = "feature", branches = mapOf("master" to 2, "feature" to 3)) },
validator = repoPredicate { repo -> repo.headBranch == "feature" && (repo.branches["feature"] ?: 0) >= (repo.branches["master"] ?: 0) },
testCases = listOf(
levelTestCase("rebase feature on master", "git rebase master"),
levelTestCase("checkout then rebase", "git checkout feature", "git rebase master"),
),
)

View File

@@ -0,0 +1,22 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `rebase_onto` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun rebaseOntoLevel(): Level = level(
id = "rebase_onto",
title = "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"),
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)) },
validator = repoPredicate { repo -> (repo.branches["readme-update"] ?: 0) == (repo.branches["master"] ?: 0) + 1 },
testCases = listOf(
levelTestCase("rebase onto explicit branch", "git rebase --onto master wrong_branch readme-update"),
levelTestCase("rebase onto current branch", "git rebase --onto master wrong_branch"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `remote_add` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun remoteAddLevel(): Level = level(
id = "remote_add",
title = "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."),
commandSuggestions = listOf("git remote add origin https://github.com/githug/githug"),
setup = { RepoState(initialized = true, branches = mapOf("master" to 0)) },
validator = repoPredicate { repo -> repo.remotes["origin"] == "https://github.com/githug/githug" },
testCases = listOf(
levelTestCase("add origin remote", "git remote add origin https://github.com/githug/githug"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `remote` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun remoteLevel(): Level = level(
id = "remote",
title = "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."),
commandSuggestions = listOf("git remote"),
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"),
testCases = listOf(
levelTestCase("answer remote name", "my_remote_repo"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `remote_url` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun remoteUrlLevel(): Level = level(
id = "remote_url",
title = "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."),
commandSuggestions = listOf("git remote -v"),
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"),
testCases = listOf(
levelTestCase("answer remote url", "https://github.com/githug/not_a_repo"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `rename_commit` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun renameCommitLevel(): Level = level(
id = "rename_commit",
title = "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."),
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)) },
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"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `rename` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun renameLevel(): Level = level(
id = "rename",
title = "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`."),
commandSuggestions = listOf("git mv oldfile.txt newfile.txt"),
setup = { RepoState(initialized = true, files = listOf(GitFile("oldfile.txt", tracked = true)), commits = listOf(CommitNode("0000001", "Committed oldfile.txt")), branches = mapOf("master" to 1)) },
validator = repoPredicate { repo -> repo.files.any { it.name == "newfile.txt" && it.staged } && repo.files.none { it.name == "oldfile.txt" } },
testCases = listOf(
levelTestCase("git mv", "git mv oldfile.txt newfile.txt"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `reorder` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun reorderLevel(): Level = level(
id = "reorder",
title = "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."),
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)) },
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"),
),
)

View File

@@ -0,0 +1,22 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `repack` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun repackLevel(): Level = level(
id = "repack",
title = "Repack",
description = "Optimise how your repository is packaged ensuring that redundant packs are removed.",
hints = listOf("You want to research the `git repack` command."),
commandSuggestions = listOf("git repack -d"),
setup = { RepoState(initialized = true, files = listOf(GitFile("foo", tracked = true)), commits = listOf(CommitNode("1", "Added foo")), branches = mapOf("master" to 1)) },
validator = repoPredicate { repo -> "repack" in repo.maintenanceActions },
testCases = listOf(
levelTestCase("repack delete redundant packs", "git repack -d"),
levelTestCase("repack all", "git repack -a -d"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `reset` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun resetLevel(): Level = level(
id = "reset",
title = "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."),
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)) },
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"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `reset_soft` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun resetSoftLevel(): Level = level(
id = "reset_soft",
title = "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`?"),
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)) },
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^"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `restore` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun restoreLevel(): Level = level(
id = "restore",
title = "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`?"),
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)) },
validator = repoPredicate { repo -> repo.files.any { it.name == "file3" && it.tracked } },
testCases = listOf(
levelTestCase("checkout file from reflog commit", "git checkout HEAD@{1} -- file3"),
),
)

View File

@@ -0,0 +1,22 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `restructure` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun restructureLevel(): Level = level(
id = "restructure",
title = "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`."),
commandSuggestions = listOf("mkdir src", "git mv *.html src"),
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 { repo -> listOf("src/about.html", "src/contact.html", "src/index.html").all { target -> repo.files.any { it.name == target && it.staged } } },
testCases = listOf(
levelTestCase("move one by one", "mkdir src", "git mv about.html src/about.html", "git mv contact.html src/contact.html", "git mv index.html src/index.html"),
levelTestCase("move with wildcard", "mkdir src", "git mv *.html src"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `revert` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun revertLevel(): Level = level(
id = "revert",
title = "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."),
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")) },
validator = repoPredicate { repo -> repo.commits.any { it.message.startsWith("Revert") } },
testCases = listOf(
levelTestCase("revert middle commit", "git revert HEAD~1"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `rm_cached` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun rmCachedLevel(): Level = level(
id = "rm_cached",
title = "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."),
commandSuggestions = listOf("git status", "git rm --cached deleteme.rb"),
setup = { RepoState(initialized = true, files = listOf(GitFile("deleteme.rb", staged = true), GitFile(".gitignore", staged = true)), branches = mapOf("master" to 0)) },
validator = repoPredicate { repo -> repo.files.any { it.name == "deleteme.rb" && !it.staged && !it.tracked && !it.deleted } },
testCases = listOf(
levelTestCase("rm cached", "git rm --cached deleteme.rb"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `rm` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun rmLevel(): Level = level(
id = "rm",
title = "Rm",
description = "A file has been removed from the working tree, but not from the repository. Identify this file and remove it.",
hints = emptyList(),
commandSuggestions = listOf("git status", "git rm deleteme.rb"),
setup = { RepoState(initialized = true, files = listOf(GitFile("deleteme.rb", tracked = true, deleted = true)), commits = listOf(CommitNode("0000001", "Added a temp file")), branches = mapOf("master" to 1)) },
validator = repoPredicate { repo -> repo.files.none { it.name == "deleteme.rb" } },
testCases = listOf(
levelTestCase("git rm deleted path", "git rm deleteme.rb"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `squash` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun squashLevel(): Level = level(
id = "squash",
title = "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."),
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)) },
validator = repoPredicate { repo -> repo.commits.size <= 2 && repo.commits.any { it.message == "Adding README" } },
testCases = listOf(
levelTestCase("interactive squash", "git rebase -i HEAD~4"),
),
)

View File

@@ -0,0 +1,22 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `stage_lines` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun stageLinesLevel(): Level = level(
id = "stage_lines",
title = "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."),
commandSuggestions = listOf("git add -p feature.rb"),
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 = repoPredicate { repo -> repo.files.any { it.name == "feature.rb" && it.staged } },
testCases = listOf(
levelTestCase("patch add feature file", "git add -p feature.rb"),
levelTestCase("patch long option", "git add --patch feature.rb"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `stash` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun stashLevel(): Level = level(
id = "stash",
title = "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."),
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)) },
validator = repoPredicate { repo -> repo.stashes.isNotEmpty() && repo.files.none { it.staged } },
testCases = listOf(
levelTestCase("stash changes", "git stash"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `status` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun statusLevel(): Level = level(
id = "status",
title = "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."),
commandSuggestions = listOf("git status"),
setup = { RepoState(initialized = true, files = listOf(GitFile("database.yml"), GitFile("README", tracked = true)), branches = mapOf("master" to 0)) },
validator = commandAnswer("database.yml"),
testCases = listOf(
levelTestCase("answer untracked file", "database.yml"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `submodule` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun submoduleLevel(): Level = level(
id = "submodule",
title = "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`."),
commandSuggestions = listOf("git submodule add https://github.com/jackmaney/githug-include-me ./githug-include-me"),
setup = { RepoState(initialized = true, branches = mapOf("master" to 0)) },
validator = repoPredicate { repo -> repo.submodules["./githug-include-me"] == "https://github.com/jackmaney/githug-include-me" || repo.submodules["githug-include-me"] == "https://github.com/jackmaney/githug-include-me" },
testCases = listOf(
levelTestCase("add submodule", "git submodule add https://github.com/jackmaney/githug-include-me ./githug-include-me"),
),
)

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
/**
* Port of the upstream ruby-githug `tag` level.
*
* Setup documents the repository shape the learner explores. Evaluation is kept
* state-based whenever the exercise changes repository objects; answer-only
* levels intentionally validate the answer entered at the prompt.
*/
internal fun tagLevel(): Level = level(
id = "tag",
title = "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 { repo -> "new_tag" in repo.tags },
testCases = listOf(
levelTestCase("create tag", "git tag new_tag"),
),
)

View File

@@ -7,10 +7,12 @@ import java.io.File
class LevelSolutionsTest {
@Test
fun everyLevelHasAKnownSolution() {
assertEquals(
allGithugLevels().map { it.id }.toSet(),
SOLUTIONS.keys,
fun everyLevelHasEmbeddedSolutionScenarios() {
val missing = allGithugLevels().filter { it.testCases.isEmpty() }.map { it.id }
assertTrue(
"Expected every level source file to define at least one solution scenario. Missing:\n${missing.joinToString("\n")}",
missing.isEmpty(),
)
}
@@ -21,6 +23,11 @@ class LevelSolutionsTest {
assertEquals(ids, ids.distinct())
}
@Test
fun levelOrderMatchesUpstreamRubyGithug() {
assertEquals(UPSTREAM_LEVEL_ORDER, allGithugLevels().map { it.id })
}
@Test
fun knownSolutionsCompleteEveryLevel() {
val evidence = StringBuilder()
@@ -30,44 +37,48 @@ class LevelSolutionsTest {
evidence.appendLine("Levels: ${allGithugLevels().size}")
evidence.appendLine()
val failures = allGithugLevels().mapNotNull { level ->
val commands = SOLUTIONS.getValue(level.id)
var repo = level.setup()
var solved = false
val failures = allGithugLevels().flatMap { level ->
evidence.appendLine("================================================================================")
evidence.appendLine("Level: ${level.id}")
evidence.appendLine("Title: ${level.title}")
evidence.appendLine("Initial repo:")
evidence.append(repo.describeForEvidence().prependIndent(" "))
evidence.appendLine("Solution commands:")
commands.forEachIndexed { index, command ->
evidence.appendLine(" ${index + 1}. $command")
}
evidence.append(level.setup().describeForEvidence().prependIndent(" "))
evidence.appendLine()
commands.forEachIndexed { index, command ->
val (nextRepo, output) = GitSandboxEngine.execute(repo, command)
repo = nextRepo
solved = level.validator(repo, command)
level.testCases.mapNotNull { testCase ->
var repo = level.setup()
var solved = false
evidence.appendLine("Command ${index + 1}: $command")
evidence.appendLine("Output:")
if (output.isEmpty()) {
evidence.appendLine(" <no output>")
} else {
output.forEach { line -> evidence.appendLine(" $line") }
evidence.appendLine("Scenario: ${testCase.name}")
evidence.appendLine("Solution commands:")
testCase.commands.forEachIndexed { index, command ->
evidence.appendLine(" ${index + 1}. $command")
}
evidence.appendLine("Repo after command:")
evidence.append(repo.describeForEvidence().prependIndent(" "))
evidence.appendLine("Validator passed after command: $solved")
evidence.appendLine()
testCase.commands.forEachIndexed { index, command ->
val (nextRepo, output) = GitSandboxEngine.execute(repo, command)
repo = nextRepo
solved = level.validator(repo, command)
evidence.appendLine("Command ${index + 1}: $command")
evidence.appendLine("Output:")
if (output.isEmpty()) {
evidence.appendLine(" <no output>")
} else {
output.forEach { line -> evidence.appendLine(" $line") }
}
evidence.appendLine("Repo after command:")
evidence.append(repo.describeForEvidence().prependIndent(" "))
evidence.appendLine("Validator passed after command: $solved")
evidence.appendLine()
}
evidence.appendLine("Scenario result: ${if (solved) "PASS" else "FAIL"}")
evidence.appendLine()
if (solved) null else "${level.id} / ${testCase.name}: ${testCase.commands.joinToString(" && ")}"
}
evidence.appendLine("Final result: ${if (solved) "PASS" else "FAIL"}")
evidence.appendLine()
if (solved) null else "${level.id}: ${commands.joinToString(" && ")}"
}
writeEvidenceLog(evidence.toString())
@@ -99,6 +110,12 @@ class LevelSolutionsTest {
appendLine("tags=${tags.sorted()}")
appendLine("remotes=${remotes.toSortedMap()}")
appendLine("config=${config.toSortedMap()}")
appendLine("stashes=$stashes")
appendLine("fetchedBranches=${fetchedBranches.sorted()}")
appendLine("pushedBranches=${pushedBranches.sorted()}")
appendLine("pushedTags=${pushedTags.sorted()}")
appendLine("submodules=${submodules.toSortedMap()}")
appendLine("maintenanceActions=${maintenanceActions.sorted()}")
appendLine("files=")
if (files.isEmpty()) {
appendLine(" <none>")
@@ -122,68 +139,64 @@ class LevelSolutionsTest {
return lineSequence().joinToString("\\n", prefix = "\"", postfix = "\"")
}
val SOLUTIONS = mapOf(
"init" to listOf("git init"),
"config" to listOf("git config user.name GitHug", "git config user.email githug@example.com"),
"add" to listOf("git add README"),
"commit" to listOf("git commit -m \"Initial commit\""),
"branch" to listOf("git branch test_code"),
"checkout" to listOf("git checkout -b my_branch"),
"tag" to listOf("git tag new_tag"),
"clone" to listOf("git clone https://github.com/Gazler/cloneme"),
"clone_to_folder" to listOf("git clone https://github.com/Gazler/cloneme my_cloned_repo"),
"ignore" to listOf("echo *.swp >> .gitignore"),
"include" to listOf("echo *.a >> .gitignore", "echo !lib.a >> .gitignore"),
"status" to listOf("database.yml"),
"number_of_files_committed" to listOf("2"),
"rm" to listOf("git rm deleteme.rb"),
"rm_cached" to listOf("git rm --cached deleteme.rb"),
"stash" to listOf("git stash"),
"rename" to listOf("git mv oldfile.txt newfile.txt"),
"restructure" to listOf(
"mkdir src",
"git mv about.html src/about.html",
"git mv contact.html src/contact.html",
"git mv index.html src/index.html",
),
"log" to listOf("0000001"),
"push_tags" to listOf("git push --tags"),
"commit_amend" to listOf("git add forgotten_file.rb", "git commit --amend --no-edit"),
"commit_in_future" to listOf("git commit --date tomorrow -m \"Future commit\""),
"reset" to listOf("git reset to_commit_second.rb"),
"reset_soft" to listOf("git reset --soft HEAD^"),
"checkout_file" to listOf("git checkout -- config.rb"),
"remote" to listOf("my_remote_repo"),
"remote_url" to listOf("https://github.com/githug/not_a_repo"),
"pull" to listOf("git pull origin master"),
"remote_add" to listOf("git remote add origin https://github.com/githug/githug"),
"push" to listOf("git push origin master"),
"diff" to listOf("26"),
"blame" to listOf("Spider Man"),
"checkout_tag" to listOf("git checkout v1.2"),
"checkout_tag_over_branch" to listOf("git checkout tags/v1.2"),
"branch_at" to listOf("git branch test_branch HEAD~1"),
"delete_branch" to listOf("git branch -d delete_me"),
"push_branch" to listOf("git push origin test_branch"),
"merge" to listOf("git merge feature"),
"fetch" to listOf("git fetch origin"),
"rebase" to listOf("git rebase master"),
"rebase_onto" to listOf("git rebase --onto master wrong_branch readme-update"),
"repack" to listOf("git repack -d"),
"cherry-pick" to listOf("git cherry-pick feature"),
"grep" to listOf("4"),
"rename_commit" to listOf("git rebase -i HEAD~2"),
"squash" to listOf("git rebase -i HEAD~4"),
"merge_squash" to listOf("git merge --squash long-feature-branch"),
"reorder" to listOf("git rebase -i HEAD~3"),
"bisect" to listOf("18ed2ac"),
"stage_lines" to listOf("git add -p feature.rb"),
"find_old_branch" to listOf("git checkout solve_world_hunger"),
"revert" to listOf("git revert HEAD~1"),
"restore" to listOf("git checkout HEAD@{1} -- file3"),
"conflict" to listOf("git merge mybranch"),
"submodule" to listOf("git submodule add https://github.com/jackmaney/githug-include-me ./githug-include-me"),
"contribute" to listOf("Open a pull request"),
val UPSTREAM_LEVEL_ORDER = listOf(
"init",
"config",
"add",
"commit",
"clone",
"clone_to_folder",
"ignore",
"include",
"status",
"number_of_files_committed",
"rm",
"rm_cached",
"stash",
"rename",
"restructure",
"log",
"tag",
"push_tags",
"commit_amend",
"commit_in_future",
"reset",
"reset_soft",
"checkout_file",
"remote",
"remote_url",
"pull",
"remote_add",
"push",
"diff",
"blame",
"branch",
"checkout",
"checkout_tag",
"checkout_tag_over_branch",
"branch_at",
"delete_branch",
"push_branch",
"merge",
"fetch",
"rebase",
"rebase_onto",
"repack",
"cherry-pick",
"grep",
"rename_commit",
"squash",
"merge_squash",
"reorder",
"bisect",
"stage_lines",
"find_old_branch",
"revert",
"restore",
"conflict",
"submodule",
"contribute",
)
}
}