diff --git a/LevelsCompare.md b/LevelsCompare.md new file mode 100644 index 0000000..cfc5009 --- /dev/null +++ b/LevelsCompare.md @@ -0,0 +1,165 @@ +# LevelsCompare + +Comparison source: `https://github.com/Gazler/githug/` at `a613718` (`release: 0.5.1`, 2024-08-11). + +Android source compared from `app/src/main/java/solutions/tretter/githugandroid/levels/`. + +This file records the upstream Ruby setup and validation intent beside the Android Kotlin setup and validation. The Android port uses a real native Git sandbox for runtime state, but it exposes validation through `RepoState`, so some checks are expressed as equivalent state predicates rather than direct Ruby/Grit calls. + +## Setup Summary + +| Level | Upstream setup intent | Android setup intent | Parity / reason for difference | +| --- | --- | --- | --- | +| `init` | Starts outside a Git repository. | Starts with empty `RepoState`. | Equivalent. | +| `config` | Initializes a repository. | Initializes a repository. | Equivalent. | +| `add` | Initializes repo, creates untracked `README`, renames branch to `master`. | Initializes repo with untracked `README` on `master`. | Equivalent. | +| `commit` | Initializes repo, creates and stages `README`. | Initializes repo with staged `README`. | Equivalent. | +| `clone` | No local repo setup. | Empty state. | Equivalent; validation is command/network simplified. | +| `clone_to_folder` | No local repo setup. | Empty state. | Equivalent; validation is command/network simplified. | +| `ignore` | Initializes repo, creates `README.swp`, clears global excludes in `.git/config`. | Initializes repo with `.gitignore` available for editing. | Differs slightly: Android does not need a swap file fixture because validation checks `.gitignore` content directly. | +| `include` | Initializes repo, creates `first.a`, `second.a`, `lib.a`, and clears excludes. | Initializes repo with `.gitignore`, `lib.a`, and `main.a`. | Same ignore-pattern lesson; Android uses a smaller fixture. | +| `status` | Initializes repo; stages `config.rb`, `README`, `setup.rb`, `deploy.rb`, `Guardfile`; leaves `database.yml` untracked. | Native setup now creates/stages the same five files and leaves `database.yml` untracked. | Equivalent. | +| `number_of_files_committed` | Initializes repo with committed modified files plus staged/untracked files, leaving exactly two files staged for commit. | Initializes equivalent file states with two staged files. | Equivalent. | +| `rm` | Initializes repo, commits `deleteme.rb`, then deletes it from the working tree. | Initializes repo with tracked deleted `deleteme.rb`. | Equivalent. | +| `rm_cached` | Initializes repo, stages `.gitignore` and `deleteme.rb`. | Initializes repo with both files staged. | Equivalent. | +| `stash` | Copies upstream fixture containing committed `lyrics.txt` plus local modifications. | Initializes repo with tracked modified `lyrics.txt` and matching native fixture. | Equivalent. | +| `rename` | Initializes repo, commits `oldfile.txt`. | Initializes repo with tracked `oldfile.txt` and commit. | Equivalent. | +| `restructure` | Initializes repo with tracked `about.html`, `contact.html`, `index.html`. | Same tracked HTML files. | Equivalent. | +| `log` | Initializes repo with one commit whose message identifies the target. | Same one modeled commit. | Equivalent. | +| `tag` | Initializes repo with one commit. | Initializes repo with one tracked file and commit. | Equivalent. | +| `push_tags` | Creates local and remote repos, tag `tag_to_be_pushed`, and origin remote. | Native setup creates local/remote repo and tag; model records tag and remote. | Equivalent state projection. | +| `commit_amend` | Commits `README`, then creates untracked `forgotten_file.rb`. | Same committed `README` plus untracked `forgotten_file.rb`. | Equivalent. | +| `commit_in_future` | Initializes repo with staged `README`. | Initializes repo with staged `README`. | Equivalent setup. | +| `reset` | Commits `README`; stages `to_commit_first.rb` and `to_commit_second.rb`. | Same committed `README` plus both staged files. | Equivalent. | +| `reset_soft` | Commits `README`, then commits `newfile.rb`. | Same two commits and tracked files. | Equivalent. | +| `checkout_file` | Commits initial `config.rb`, then modifies it. | Same tracked modified `config.rb`. | Equivalent. | +| `remote` | Initializes repo and adds `my_remote_repo` remote. | Same remote in initialized repo. | Equivalent. | +| `remote_url` | Initializes repo and adds `my_remote_repo` and `remote_location`. | Same two remotes. | Equivalent. | +| `pull` | Initializes repo and adds remote URL `https://github.com/pull-this/thing-to-pull`. | Native setup creates local synthetic remote with a remote commit. | Deliberate offline equivalent to upstream network remote. | +| `remote_add` | Initializes repo with no origin remote. | Same. | Equivalent. | +| `push` | Creates divergent local and remote master histories: local has `file3`, remote has `file4`. | Native setup creates synthetic divergent origin with the same file/commit shape. | Equivalent. | +| `diff` | Copies upstream fixture with committed `app.rb` and one changed line. | Native setup creates committed then modified `app.rb`. | Equivalent. | +| `blame` | Copies upstream fixture where password line is authored by Spider Man. | Native setup creates deterministic author history for `config.rb`. | Equivalent. | +| `branch` | Initializes repo with one `README` commit. | Same. | Equivalent. | +| `checkout` | Initializes repo with one `README` commit. | Same. | Equivalent. | +| `checkout_tag` | Creates five commits and tags `v1.0`, `v1.2`, `v1.5`. | Same commit/tag shape. | Equivalent. | +| `checkout_tag_over_branch` | Creates same tags and also a branch named `v1.2`. | Same tag-plus-branch ambiguity. | Equivalent. | +| `branch_at` | Creates three commits on `file1`. | Same three modeled commits and native setup. | Equivalent. | +| `delete_branch` | Copies fixture with `delete_me` branch. | Initializes repo with `delete_me` branch. | Equivalent. | +| `push_branch` | Creates local and remote repos with multiple branches and target `test_branch`. | Native setup creates synthetic remote and same branch intent. | Equivalent state projection. | +| `merge` | Copies fixture with `file1` on master and `file2` on feature. | Native setup creates master `file1` and feature `file2`. | Equivalent. | +| `fetch` | Creates local master tracking origin/master; remote later gains `new_branch` with `file1`. | Native setup creates the same synthetic remote branch scenario; model starts with one fetched head. | Equivalent after latest update. | +| `rebase` | Copies fixture with divergent `master` and `feature`. | Native setup creates matching divergent `master` and `feature`. | Equivalent. | +| `rebase_onto` | Creates `authors.md`, bad `wrong_branch`, and `readme-update` branch needing `--onto`. | Native setup creates the same branch graph and files. | Equivalent setup. | +| `repack` | Initializes repo with one committed `foo`. | Same. | Equivalent. | +| `cherry-pick` | Copies fixture with `master` and `new-feature` commits. | Native setup creates equivalent branches/files. | Equivalent. | +| `grep` | Copies fixture with four TODO entries. | Initializes `app.rb` with four TODO entries. | Equivalent. | +| `rename_commit` | Creates commits `Initial commit`, `First coommit`, `Second commit`. | Same typo commit sequence. | Equivalent. | +| `squash` | Creates initial hidden commit plus README and three squash-target README updates. | Same commit sequence. | Equivalent. | +| `merge_squash` | Creates master and `long-feature-branch` with multiple feature changes. | Native setup creates the same branch and `file3` feature effect. | Mostly equivalent; Android currently validates less detail. | +| `reorder` | Creates commits `Initial Setup`, `First commit`, `Third commit`, `Second commit`. | Same order. | Equivalent. | +| `bisect` | Copies upstream bisect fixture. | Initializes modeled bad hash `18ed2ac`. | Simplified setup; Android validates the intended answer directly. | +| `stage_lines` | Commits initial `feature.rb`, then leaves two unstaged feature lines. | Initializes tracked `feature.rb` containing both feature lines. | Setup is close, but Android does not yet model partial staged vs unstaged hunks. | +| `find_old_branch` | Copies fixture with recoverable `solve_world_hunger` branch. | Initializes branch map with `solve_world_hunger`. | Equivalent for visible exercise state. | +| `revert` | Creates commits `First commit`, `Bad commit`, `Second commit`. | Same commit messages. | Equivalent. | +| `restore` | Creates `file1`, `file2`, then creates and removes `file3` so it is recoverable from reflog/history. | Native setup creates matching history and removes `file3`; model starts without `file3`. | Equivalent. | +| `conflict` | Copies fixture with `master` and `mybranch` conflict in `poem.txt`. | Initializes equivalent branch/file conflict fixture. | Equivalent setup. | +| `submodule` | Initializes empty repo. | Same. | Equivalent; network submodule operation is modeled. | +| `contribute` | No local setup; solution clones upstream externally. | Empty state. | Deliberate simplification for mobile/offline final prompt. | + +## Validation Summary + +| Level | Upstream validation intent | Android validation intent | Parity / reason for difference | +| --- | --- | --- | --- | +| `init` | `repo.valid?` | `RepoState.initialized` | Equivalent. | +| `config` | Prompted name/email must match `user.name` and `user.email`. | Validates nonblank `user.name` and `user.email`. | Deliberate UI difference: Android validates repository config state, not repeated prompt text. | +| `add` | `README` exists in index and is not untracked. | `README` is staged. | Equivalent. | +| `commit` | Repository has at least one commit. | Commit exists and `README` became tracked/unstaged. | Stricter but same lesson outcome. | +| `clone` | `cloneme` repo contains known commit `157b2b6...`. | Accepts exact clone command. | Deliberate mobile/network simplification; no external GitHub clone is performed in-game. | +| `clone_to_folder` | `my_cloned_repo` contains known commit. | Accepts exact clone-to-folder command. | Deliberate mobile/network simplification. | +| `ignore` | `.gitignore` contains `*.swp`. | `.gitignore` has a line equal to `*.swp`. | Equivalent. | +| `include` | `.gitignore` contains `*.a` and `!lib.a`. | Same two entries are present. | Equivalent. | +| `status` | User answers `database.yml`; setup has five staged files and one untracked file. | User answers `database.yml`; setup now mirrors staged files plus untracked `database.yml`. | Updated for parity. | +| `number_of_files_committed` | Prompt answer is integer `2`. | Answer is `2`. | Equivalent. | +| `rm` | `deleteme.rb` no longer has an unstaged deletion. | File removed from model or deletion staged. | Equivalent. | +| `rm_cached` | `deleteme.rb` is not staged and still exists. | `deleteme.rb` is unstaged, untracked, and not deleted. | Equivalent. | +| `stash` | `stash@{0}` exists and `lyrics.txt` has no working-tree changes. | At least one stash and no staged files. | Slightly looser; Android does not model unstaged tracked dirt separately after stash. | +| `rename` | `oldfile.txt` deleted unstaged and `newfile.txt` added. | `newfile.txt` exists and live `oldfile.txt` is gone. | Equivalent for accepted solutions. | +| `restructure` | HTML files are deleted at root and added under `src/`. | All three `src/*.html` files exist and no live root HTML files remain. | Equivalent. | +| `log` | Prompt answer matches latest commit hash prefix. | Answer matches the modeled commit hash. | Equivalent within Android's deterministic commit model. | +| `tag` | First tag is `new_tag`. | `new_tag` exists. | Equivalent. | +| `push_tags` | Remote tag list contains `tag_to_be_pushed`. | `tag_to_be_pushed` is in `pushedTags`. | Equivalent state projection. | +| `commit_amend` | One commit and amended commit contains two files. | One commit and `forgotten_file.rb` is tracked. | Equivalent. | +| `commit_in_future` | Commit authored date is in the future. | Any commit exists. | Known gap: Android does not currently expose commit authored timestamps in `RepoState`. | +| `reset` | `to_commit_second.rb` exists but is unstaged; `to_commit_first.rb` remains staged. | Same staged/unstaged split with one commit. | Equivalent. | +| `reset_soft` | `newfile.rb` exists, is staged, and commit count is one. | Same. | Equivalent. | +| `checkout_file` | `config.rb` no longer modified and commit count remains one. | `config.rb` content equals initial content. | Equivalent. | +| `remote` | User answers `my_remote_repo`. | Answer is `my_remote_repo`. | Equivalent. | +| `remote_url` | User answers URL matching `https://github.com/githug/not_a_repo/?`. | Answer is exact URL without trailing slash. | Slightly stricter; can be relaxed if trailing slash should be accepted. | +| `pull` | Latest commit hash is `1797a7c`. | Remote file is present, `origin/master` fetched, and commits advanced. | Equivalent local synthetic remote outcome. | +| `remote_add` | `git remote -v` contains `https://github.com/githug/githug`. | `origin` remote equals that URL. | Equivalent and slightly stricter on remote name. | +| `push` | Local `master` and `origin/master` have four identical commits. | `origin/master` was pushed, four files are present, and commit count is at least four. | Equivalent user outcome; Android tracks pushed ref instead of comparing remote commit IDs. | +| `diff` | Answer is changed line number `26`. | Answer is `26`. | Equivalent. | +| `blame` | Answer equals author of known password commit (`Spider Man`). | Answer is `Spider Man`. | Equivalent with deterministic Android fixture. | +| `branch` | Branch `test_code` exists. | `test_code` exists and current branch remains `master`. | Slightly stricter to prevent solving by checkout side effects. | +| `checkout` | Current branch is `my_branch`. | Current branch is `my_branch` and branch exists. | Equivalent. | +| `checkout_tag` | HEAD subject is "Some more changes" with five commits. | HEAD is at tag `v1.2`. | Equivalent in Android tag model. | +| `checkout_tag_over_branch` | Same as `checkout_tag`; must choose tag, not branch. | HEAD is at tag `v1.2`. | Equivalent in Android tag model. | +| `branch_at` | `test_branch` exists and excludes "Updating file1 again". | `test_branch` points at commit index 2. | Equivalent. | +| `delete_branch` | `delete_me` branch no longer exists. | Same. | Equivalent. | +| `push_branch` | Remote has pushed `test_branch` but not all branches. | `origin/test_branch` pushed and master/other not pushed. | Equivalent. | +| `merge` | `file1` and `file2` exist. | Current branch is `master`, a merge action occurred, and `file2` is tracked. | Deliberately stricter than upstream to avoid `git switch feature` falsely solving on Android. | +| `fetch` | Local branch count is one and `.git/FETCH_HEAD` has two entries. | `branches.size == 1`, `fetchHeadCount == 2`, and no recorded `pull` action. | Mostly parity; Android adds the `pull` guard because real Git can leave two `FETCH_HEAD` lines after `git pull`, while the exercise wording explicitly says fetch without merging. | +| `rebase` | `feature` commit messages are `add feature`, `add content`, `init commit`, and old hash changed. | Current branch is `feature` and modeled commit messages match. | Equivalent, except Android does not compare the old hash. | +| `rebase_onto` | `readme-update` has four commits, excludes "Wrong changes", and preserves authors. | Current branch is `readme-update` and rebase-onto action occurred. | Known gap: Android validation is looser than upstream content/commit checks. | +| `repack` | `git count-objects -v` includes packed/pruned object evidence. | `repack` action recorded. | Equivalent action-level validation; Android does not model object database packing stats. | +| `cherry-pick` | Top commits are "Filled in README..." then "Added fancy branded output". | Same commit message order plus `README.md` tracked. | Equivalent. | +| `grep` | Answer is TODO count `4`. | Answer is `4`. | Equivalent. | +| `rename_commit` | Parent commit message is corrected to `First commit`. | No `coommit` remains and `First commit` exists. | Equivalent for intended reword. | +| `squash` | Commit count is two. | Commit count at most two and "Adding README" remains. | Slightly stricter on preserving the base README commit. | +| `merge_squash` | Commit count is three and all long-feature changes are included. | Squash action recorded and a commit exists. | Known gap: Android does not yet verify all squash file/content effects. | +| `reorder` | `git log` subject order matches `Third.*Second.*First.*Initial`. | Modeled commit order becomes First, Second, Third. | Equivalent relative to Android's oldest-first commit list. | +| `bisect` | Answer is hash prefix `18ed2ac`. | Answer is `18ed2ac`. | Equivalent. | +| `stage_lines` | Staged diff contains first feature line; unstaged diff contains second feature line. | `feature.rb` is staged. | Known gap: Android does not yet model partial hunk staging. | +| `find_old_branch` | Current branch is `solve_world_hunger`. | Same. | Equivalent. | +| `revert` | More than three commits and a revert of "Bad commit" exists. | A commit message starts with `Revert`. | Slightly looser; sufficient for current fixture. | +| `restore` | `file3` exists. | `file3` is tracked. | Equivalent. | +| `conflict` | On `master`, merge commit has two parents, conflict markers removed, both poem lines preserved. | `poem.txt` contains expected merged poem text. | Known gap: Android does not model merge-parent count for conflict resolution. | +| `submodule` | `githug-include-me` directory exists, has README, and is a gitlink/submodule. | `submodules` contains `githug-include-me` URL. | Equivalent state projection. | +| `contribute` | Clones upstream and checks for a commit authored by configured user. | Any nonblank command. | Deliberate mobile/offline simplification for the final contribution prompt. | + +## Focused Source-To-Android Checks + +### `fetch` + +| Upstream Ruby (`levels/fetch.rb`) | Android Kotlin (`FetchLevel.kt`) | +| --- | --- | +| Counts local branches with `repo.branches.size`. | Uses `repo.branches.size`. | +| Counts `.git/FETCH_HEAD` lines after fetch; success requires `num_remote == 2`. | `GitRuntime` now reads `.git/FETCH_HEAD` into `RepoState.fetchHeadCount`; success requires `fetchHeadCount == 2`. | +| Success requires exactly one local branch and two fetched heads. | Success requires exactly one local branch and two fetched heads. Android also rejects a recorded `pull` action to preserve the exercise instruction "without merging" under native Git behavior. | + +### `status` + +| Upstream Ruby (`levels/status.rb`) | Android Kotlin (`StatusLevel.kt`) | +| --- | --- | +| Stages `config.rb`, `README`, `setup.rb`, `deploy.rb`, and `Guardfile`. | Native setup now creates and stages those same five files. | +| Leaves `database.yml` untracked. | Leaves `database.yml` untracked. | +| Validates answer `database.yml`. | Validates answer `database.yml`. | + +### `merge` + +| Upstream Ruby (`levels/merge.rb`) | Android Kotlin (`MergeLevel.kt`) | +| --- | --- | +| Validates `File.exists?("file1") && File.exists?("file2")`. | Validates `master`, a recorded merge action, and tracked `file2`. | +| Upstream is filesystem-only and loose. | Android is intentionally stricter because `git switch feature` otherwise produces the same visible file state and falsely solves the level. | + +## Follow-Up Gaps + +These are the remaining known non-parity items that need additional model support if exact upstream validation is required: + +- `commit_in_future`: add authored timestamp inspection to `CommitNode`/`RepoState`. +- `stage_lines`: model partial staged vs unstaged hunks. +- `merge_squash`: verify the exact squashed file/content effects. +- `conflict`: model merge parent count or inspect merge commit parents. +- `rebase_onto`: verify final commit count/content and removal of "Wrong changes". +- `contribute`, `clone`, `clone_to_folder`: current Android behavior intentionally avoids real network-dependent validation. diff --git a/README.md b/README.md index 09df894..eb92b28 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ The UI supports a terminal-centered workflow with optional inspection panes: Development tasks should be run through the project tooling script. It provisions the local toolchain, keeps paths project-relative, and runs Gradle with the project configuration expected by this repository. +While implementing changes, keep `commit-summary.txt` updated as you go. This file is intentionally ignored by git and excluded from automatic commits so it can be edited manually too; the build tooling uses it as the commit message when it exists and is non-empty. + ## Android project tooling This repo includes a root-level tooling script: @@ -133,6 +135,7 @@ When adding or changing a level: - Keep one source file per exercise. - Keep `allGithugLevels()` in upstream Ruby Githug order. - Document the setup and validation intent in the level file. +- Update `LevelsCompare.md` whenever level setup, validation, hints, or accepted solution behavior changes. - Prefer validators that inspect `RepoState` over validators that match command strings. - Add multiple `LevelTestCase` scenarios when more than one solution path should be accepted. - Run `bash ./AndroidProjectTooling.sh --test` before building. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ce713cc..3151cd9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -19,8 +19,8 @@ android { applicationId = "solutions.tretter.githugandroid" minSdk = 26 targetSdk = 35 - versionCode = 135 - versionName = "0.1.134" + versionCode = 136 + versionName = "0.1.135" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true diff --git a/app/src/main/java/solutions/tretter/githugandroid/GameModels.kt b/app/src/main/java/solutions/tretter/githugandroid/GameModels.kt index e2ec149..26fdbe3 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/GameModels.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/GameModels.kt @@ -31,6 +31,7 @@ data class RepoState( val config: Map = emptyMap(), val stashes: List = emptyList(), val fetchedBranches: Set = emptySet(), + val fetchHeadCount: Int = 0, val pushedBranches: Set = emptySet(), val pushedTags: Set = emptySet(), val submodules: Map = emptyMap(), @@ -55,4 +56,3 @@ data class LevelTestCase( ) fun sampleLevels(): List = allGithugLevels() - diff --git a/app/src/main/java/solutions/tretter/githugandroid/GitRuntime.kt b/app/src/main/java/solutions/tretter/githugandroid/GitRuntime.kt index 1c5580d..3ac20e9 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/GitRuntime.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/GitRuntime.kt @@ -631,6 +631,11 @@ class GitRepositoryRuntime private constructor( val exactTagResult = runGit(nativeGit, sandbox, listOf("describe", "--tags", "--exact-match")) val userNameResult = runGit(nativeGit, sandbox, listOf("config", "--get", "user.name")) val userEmailResult = runGit(nativeGit, sandbox, listOf("config", "--get", "user.email")) + val fetchHeadCount = File(sandbox, ".git/FETCH_HEAD") + .takeIf { it.isFile } + ?.readLines() + ?.count { it.isNotBlank() } + ?: 0 val config = buildMap { userNameResult.outputLines.firstOrNull() ?.takeIf { userNameResult.exitCode == 0 && it.isNotBlank() } @@ -704,6 +709,7 @@ class GitRepositoryRuntime private constructor( .map { it.removePrefix("*").trim() } .filter { it.isNotBlank() && " -> " !in it } .toSet(), + fetchHeadCount = fetchHeadCount, ) } @@ -738,9 +744,8 @@ class GitRepositoryRuntime private constructor( 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" + "$remote/new_branch", + fetchedBranches = inspectedRepo.fetchedBranches + previousRepo.fetchedBranches, maintenanceActions = inspectedRepo.maintenanceActions + previousRepo.maintenanceActions + "fetch", ) } @@ -749,6 +754,8 @@ class GitRepositoryRuntime private constructor( val branch = tokens.drop(2).lastOrNull()?.takeIf { !it.startsWith("-") && it != remote } ?: inspectedRepo.headBranch inspectedRepo.copy( fetchedBranches = inspectedRepo.fetchedBranches + previousRepo.fetchedBranches + "$remote/$branch", + fetchHeadCount = inspectedRepo.fetchHeadCount, + maintenanceActions = inspectedRepo.maintenanceActions + previousRepo.maintenanceActions + "pull", ) } "push" -> { @@ -795,6 +802,7 @@ class GitRepositoryRuntime private constructor( else -> inspectedRepo.copy( stashes = mergeDistinct(previousRepo.stashes, inspectedRepo.stashes), fetchedBranches = previousRepo.fetchedBranches + inspectedRepo.fetchedBranches, + fetchHeadCount = inspectedRepo.fetchHeadCount, pushedBranches = previousRepo.pushedBranches + inspectedRepo.pushedBranches, pushedTags = previousRepo.pushedTags + inspectedRepo.pushedTags, submodules = previousRepo.submodules + inspectedRepo.submodules, diff --git a/app/src/main/java/solutions/tretter/githugandroid/GitSandboxEngine.kt b/app/src/main/java/solutions/tretter/githugandroid/GitSandboxEngine.kt index f1bf1cb..53c776f 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/GitSandboxEngine.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/GitSandboxEngine.kt @@ -63,7 +63,8 @@ object GitSandboxEngine { 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", "$remote/new_branch"), + fetchedBranches = repo.fetchedBranches + listOf("$remote/master", "$remote/new_branch"), + fetchHeadCount = 2, maintenanceActions = repo.maintenanceActions + "fetch", ) to emptyList() } @@ -72,7 +73,9 @@ object GitSandboxEngine { val branch = parts.drop(2).lastOrNull()?.takeIf { !it.startsWith("-") && it != remote } ?: repo.headBranch repo.copy( fetchedBranches = repo.fetchedBranches + "$remote/$branch", + fetchHeadCount = 1, branches = repo.branches + (repo.headBranch to maxOf(repo.branches[repo.headBranch] ?: 0, 2)), + maintenanceActions = repo.maintenanceActions + "pull", ) to emptyList() } parts.size >= 2 && parts[1] == "push" -> pushRefs(repo, parts.drop(2)) diff --git a/app/src/main/java/solutions/tretter/githugandroid/RepoStateSaver.kt b/app/src/main/java/solutions/tretter/githugandroid/RepoStateSaver.kt index be4d560..6028f71 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/RepoStateSaver.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/RepoStateSaver.kt @@ -20,6 +20,7 @@ val RepoStateSaver = listSaver( state.pushedTags.toList(), state.submodules.flatMap { listOf(it.key, it.value) }, state.maintenanceActions.toList(), + state.fetchHeadCount, ) }, restore = { saved -> @@ -37,6 +38,7 @@ val RepoStateSaver = listSaver( val pushedTags = saved.getOrNull(12) as? List<*> ?: emptyList() val submoduleParts = saved.getOrNull(13) as? List<*> ?: emptyList() val maintenanceActions = saved.getOrNull(14) as? List<*> ?: emptyList() + val fetchHeadCount = saved.getOrNull(15) as? Int ?: 0 RepoState( initialized = initialized, headBranch = headBranch, @@ -59,6 +61,7 @@ val RepoStateSaver = listSaver( config = configParts.chunked(2).associate { (it[0] as String) to (it[1] as String) }, stashes = stashes.filterIsInstance(), fetchedBranches = fetchedBranches.filterIsInstance().toSet(), + fetchHeadCount = fetchHeadCount, pushedBranches = pushedBranches.filterIsInstance().toSet(), pushedTags = pushedTags.filterIsInstance().toSet(), submodules = submoduleParts.chunked(2).associate { (it[0] as String) to (it[1] as String) }, diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/FetchLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/FetchLevel.kt index 29ea8d5..f6c17d7 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/FetchLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/FetchLevel.kt @@ -15,7 +15,15 @@ internal fun fetchLevel(): Level = level( 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")) }, + setup = { + RepoState( + initialized = true, + branches = mapOf("master" to 1), + remotes = mapOf("origin" to "remote"), + fetchedBranches = setOf("origin/master"), + fetchHeadCount = 1, + ) + }, nativeSetup = { resetFiles() write("master_file") @@ -33,9 +41,9 @@ internal fun fetchLevel(): Level = level( true }, validator = repoPredicate { repo -> - "fetch" in repo.maintenanceActions && - "origin/new_branch" in repo.fetchedBranches && - repo.headBranch == "master" + repo.branches.size == 1 && + repo.fetchHeadCount == 2 && + "pull" !in repo.maintenanceActions }, testCases = listOf( levelTestCase("fetch origin", "git fetch origin"), diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/LevelCatalog.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/LevelCatalog.kt index 65c62a2..c7c1def 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/LevelCatalog.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/LevelCatalog.kt @@ -149,6 +149,8 @@ private fun RepoState.validationSnapshot(): String = buildString { append(remotes.toSortedMap()) append(", fetchedBranches=") append(fetchedBranches.sorted()) + append(", fetchHeadCount=") + append(fetchHeadCount) append(", pushedBranches=") append(pushedBranches.sorted()) append(", pushedTags=") diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/StatusLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/StatusLevel.kt index 42b3f09..6a674d6 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/StatusLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/StatusLevel.kt @@ -13,7 +13,29 @@ internal fun statusLevel(): Level = level( 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")), branches = mapOf("master" to 0)) }, + setup = { + RepoState( + initialized = true, + files = listOf( + GitFile("config.rb", staged = true), + GitFile("README", staged = true), + GitFile("setup.rb", staged = true), + GitFile("deploy.rb", staged = true), + GitFile("Guardfile", staged = true), + GitFile("database.yml"), + ), + branches = mapOf("master" to 0), + ) + }, + nativeSetup = { + resetFiles() + listOf("config.rb", "README", "setup.rb", "deploy.rb", "Guardfile").forEach { file -> + write(file) + git("add", file) + } + write("database.yml") + true + }, validator = commandAnswer("database.yml"), testCases = listOf( levelTestCase("answer untracked file", "database.yml"), diff --git a/app/src/test/java/solutions/tretter/githugandroid/LevelSolutionsTest.kt b/app/src/test/java/solutions/tretter/githugandroid/LevelSolutionsTest.kt index dc233e7..1b0fe73 100644 --- a/app/src/test/java/solutions/tretter/githugandroid/LevelSolutionsTest.kt +++ b/app/src/test/java/solutions/tretter/githugandroid/LevelSolutionsTest.kt @@ -101,7 +101,8 @@ class LevelSolutionsTest { @Test fun reportedRegressionCommandsDoNotSolveLevels() { val statusRepo = statusLevel().setup() - assertEquals(listOf("database.yml"), statusRepo.files.map { it.name }) + val untrackedStatusFiles = statusRepo.files.filter { !it.staged && !it.tracked && !it.deleted }.map { it.name } + assertEquals(listOf("database.yml"), untrackedStatusFiles) val gitBinary = testGitBinary() val runtimeRoot = testSandboxRoot().apply { @@ -185,6 +186,7 @@ class LevelSolutionsTest { appendLine("config=${config.toSortedMap()}") appendLine("stashes=$stashes") appendLine("fetchedBranches=${fetchedBranches.sorted()}") + appendLine("fetchHeadCount=$fetchHeadCount") appendLine("pushedBranches=${pushedBranches.sorted()}") appendLine("pushedTags=${pushedTags.sorted()}") appendLine("submodules=${submodules.toSortedMap()}")