Align level parity docs and fetch/status validation
- Add LevelsCompare.md with setup and validation comparisons against Gazler/githug. - Document that LevelsCompare.md must be updated when level behavior changes. - Track FETCH_HEAD count in RepoState so the Fetch level can match upstream validation. - Mirror upstream Status setup with staged files plus untracked database.yml. - Keep Fetch from being solved by pull despite native Git FETCH_HEAD behavior. - Bump app version and build debug APK.
This commit is contained in:
165
LevelsCompare.md
Normal file
165
LevelsCompare.md
Normal file
@@ -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.
|
||||||
@@ -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.
|
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
|
## Android project tooling
|
||||||
|
|
||||||
This repo includes a root-level tooling script:
|
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 one source file per exercise.
|
||||||
- Keep `allGithugLevels()` in upstream Ruby Githug order.
|
- Keep `allGithugLevels()` in upstream Ruby Githug order.
|
||||||
- Document the setup and validation intent in the level file.
|
- 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.
|
- Prefer validators that inspect `RepoState` over validators that match command strings.
|
||||||
- Add multiple `LevelTestCase` scenarios when more than one solution path should be accepted.
|
- Add multiple `LevelTestCase` scenarios when more than one solution path should be accepted.
|
||||||
- Run `bash ./AndroidProjectTooling.sh --test` before building.
|
- Run `bash ./AndroidProjectTooling.sh --test` before building.
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ android {
|
|||||||
applicationId = "solutions.tretter.githugandroid"
|
applicationId = "solutions.tretter.githugandroid"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 135
|
versionCode = 136
|
||||||
versionName = "0.1.134"
|
versionName = "0.1.135"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ data class RepoState(
|
|||||||
val config: Map<String, String> = emptyMap(),
|
val config: Map<String, String> = emptyMap(),
|
||||||
val stashes: List<String> = emptyList(),
|
val stashes: List<String> = emptyList(),
|
||||||
val fetchedBranches: Set<String> = emptySet(),
|
val fetchedBranches: Set<String> = emptySet(),
|
||||||
|
val fetchHeadCount: Int = 0,
|
||||||
val pushedBranches: Set<String> = emptySet(),
|
val pushedBranches: Set<String> = emptySet(),
|
||||||
val pushedTags: Set<String> = emptySet(),
|
val pushedTags: Set<String> = emptySet(),
|
||||||
val submodules: Map<String, String> = emptyMap(),
|
val submodules: Map<String, String> = emptyMap(),
|
||||||
@@ -55,4 +56,3 @@ data class LevelTestCase(
|
|||||||
)
|
)
|
||||||
|
|
||||||
fun sampleLevels(): List<Level> = allGithugLevels()
|
fun sampleLevels(): List<Level> = allGithugLevels()
|
||||||
|
|
||||||
|
|||||||
@@ -631,6 +631,11 @@ class GitRepositoryRuntime private constructor(
|
|||||||
val exactTagResult = runGit(nativeGit, sandbox, listOf("describe", "--tags", "--exact-match"))
|
val exactTagResult = runGit(nativeGit, sandbox, listOf("describe", "--tags", "--exact-match"))
|
||||||
val userNameResult = runGit(nativeGit, sandbox, listOf("config", "--get", "user.name"))
|
val userNameResult = runGit(nativeGit, sandbox, listOf("config", "--get", "user.name"))
|
||||||
val userEmailResult = runGit(nativeGit, sandbox, listOf("config", "--get", "user.email"))
|
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 {
|
val config = buildMap {
|
||||||
userNameResult.outputLines.firstOrNull()
|
userNameResult.outputLines.firstOrNull()
|
||||||
?.takeIf { userNameResult.exitCode == 0 && it.isNotBlank() }
|
?.takeIf { userNameResult.exitCode == 0 && it.isNotBlank() }
|
||||||
@@ -704,6 +709,7 @@ class GitRepositoryRuntime private constructor(
|
|||||||
.map { it.removePrefix("*").trim() }
|
.map { it.removePrefix("*").trim() }
|
||||||
.filter { it.isNotBlank() && " -> " !in it }
|
.filter { it.isNotBlank() && " -> " !in it }
|
||||||
.toSet(),
|
.toSet(),
|
||||||
|
fetchHeadCount = fetchHeadCount,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -738,9 +744,8 @@ class GitRepositoryRuntime private constructor(
|
|||||||
stashes = mergeDistinct(previousRepo.stashes, inspectedRepo.stashes + "stash@{${previousRepo.stashes.size}}"),
|
stashes = mergeDistinct(previousRepo.stashes, inspectedRepo.stashes + "stash@{${previousRepo.stashes.size}}"),
|
||||||
)
|
)
|
||||||
"fetch" -> {
|
"fetch" -> {
|
||||||
val remote = tokens.getOrNull(2)?.takeIf { !it.startsWith("-") } ?: "origin"
|
|
||||||
inspectedRepo.copy(
|
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",
|
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
|
val branch = tokens.drop(2).lastOrNull()?.takeIf { !it.startsWith("-") && it != remote } ?: inspectedRepo.headBranch
|
||||||
inspectedRepo.copy(
|
inspectedRepo.copy(
|
||||||
fetchedBranches = inspectedRepo.fetchedBranches + previousRepo.fetchedBranches + "$remote/$branch",
|
fetchedBranches = inspectedRepo.fetchedBranches + previousRepo.fetchedBranches + "$remote/$branch",
|
||||||
|
fetchHeadCount = inspectedRepo.fetchHeadCount,
|
||||||
|
maintenanceActions = inspectedRepo.maintenanceActions + previousRepo.maintenanceActions + "pull",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
"push" -> {
|
"push" -> {
|
||||||
@@ -795,6 +802,7 @@ class GitRepositoryRuntime private constructor(
|
|||||||
else -> inspectedRepo.copy(
|
else -> inspectedRepo.copy(
|
||||||
stashes = mergeDistinct(previousRepo.stashes, inspectedRepo.stashes),
|
stashes = mergeDistinct(previousRepo.stashes, inspectedRepo.stashes),
|
||||||
fetchedBranches = previousRepo.fetchedBranches + inspectedRepo.fetchedBranches,
|
fetchedBranches = previousRepo.fetchedBranches + inspectedRepo.fetchedBranches,
|
||||||
|
fetchHeadCount = inspectedRepo.fetchHeadCount,
|
||||||
pushedBranches = previousRepo.pushedBranches + inspectedRepo.pushedBranches,
|
pushedBranches = previousRepo.pushedBranches + inspectedRepo.pushedBranches,
|
||||||
pushedTags = previousRepo.pushedTags + inspectedRepo.pushedTags,
|
pushedTags = previousRepo.pushedTags + inspectedRepo.pushedTags,
|
||||||
submodules = previousRepo.submodules + inspectedRepo.submodules,
|
submodules = previousRepo.submodules + inspectedRepo.submodules,
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ object GitSandboxEngine {
|
|||||||
parts.size >= 2 && parts[1] == "fetch" -> {
|
parts.size >= 2 && parts[1] == "fetch" -> {
|
||||||
val remote = parts.getOrNull(2)?.takeIf { !it.startsWith("-") } ?: "origin"
|
val remote = parts.getOrNull(2)?.takeIf { !it.startsWith("-") } ?: "origin"
|
||||||
repo.copy(
|
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",
|
maintenanceActions = repo.maintenanceActions + "fetch",
|
||||||
) to emptyList()
|
) to emptyList()
|
||||||
}
|
}
|
||||||
@@ -72,7 +73,9 @@ object GitSandboxEngine {
|
|||||||
val branch = parts.drop(2).lastOrNull()?.takeIf { !it.startsWith("-") && it != remote } ?: repo.headBranch
|
val branch = parts.drop(2).lastOrNull()?.takeIf { !it.startsWith("-") && it != remote } ?: repo.headBranch
|
||||||
repo.copy(
|
repo.copy(
|
||||||
fetchedBranches = repo.fetchedBranches + "$remote/$branch",
|
fetchedBranches = repo.fetchedBranches + "$remote/$branch",
|
||||||
|
fetchHeadCount = 1,
|
||||||
branches = repo.branches + (repo.headBranch to maxOf(repo.branches[repo.headBranch] ?: 0, 2)),
|
branches = repo.branches + (repo.headBranch to maxOf(repo.branches[repo.headBranch] ?: 0, 2)),
|
||||||
|
maintenanceActions = repo.maintenanceActions + "pull",
|
||||||
) to emptyList()
|
) to emptyList()
|
||||||
}
|
}
|
||||||
parts.size >= 2 && parts[1] == "push" -> pushRefs(repo, parts.drop(2))
|
parts.size >= 2 && parts[1] == "push" -> pushRefs(repo, parts.drop(2))
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ val RepoStateSaver = listSaver<RepoState, Any>(
|
|||||||
state.pushedTags.toList(),
|
state.pushedTags.toList(),
|
||||||
state.submodules.flatMap { listOf(it.key, it.value) },
|
state.submodules.flatMap { listOf(it.key, it.value) },
|
||||||
state.maintenanceActions.toList(),
|
state.maintenanceActions.toList(),
|
||||||
|
state.fetchHeadCount,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
restore = { saved ->
|
restore = { saved ->
|
||||||
@@ -37,6 +38,7 @@ val RepoStateSaver = listSaver<RepoState, Any>(
|
|||||||
val pushedTags = saved.getOrNull(12) as? List<*> ?: emptyList<Any>()
|
val pushedTags = saved.getOrNull(12) as? List<*> ?: emptyList<Any>()
|
||||||
val submoduleParts = saved.getOrNull(13) as? List<*> ?: emptyList<Any>()
|
val submoduleParts = saved.getOrNull(13) as? List<*> ?: emptyList<Any>()
|
||||||
val maintenanceActions = saved.getOrNull(14) as? List<*> ?: emptyList<Any>()
|
val maintenanceActions = saved.getOrNull(14) as? List<*> ?: emptyList<Any>()
|
||||||
|
val fetchHeadCount = saved.getOrNull(15) as? Int ?: 0
|
||||||
RepoState(
|
RepoState(
|
||||||
initialized = initialized,
|
initialized = initialized,
|
||||||
headBranch = headBranch,
|
headBranch = headBranch,
|
||||||
@@ -59,6 +61,7 @@ val RepoStateSaver = listSaver<RepoState, Any>(
|
|||||||
config = configParts.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>(),
|
stashes = stashes.filterIsInstance<String>(),
|
||||||
fetchedBranches = fetchedBranches.filterIsInstance<String>().toSet(),
|
fetchedBranches = fetchedBranches.filterIsInstance<String>().toSet(),
|
||||||
|
fetchHeadCount = fetchHeadCount,
|
||||||
pushedBranches = pushedBranches.filterIsInstance<String>().toSet(),
|
pushedBranches = pushedBranches.filterIsInstance<String>().toSet(),
|
||||||
pushedTags = pushedTags.filterIsInstance<String>().toSet(),
|
pushedTags = pushedTags.filterIsInstance<String>().toSet(),
|
||||||
submodules = submoduleParts.chunked(2).associate { (it[0] as String) to (it[1] as String) },
|
submodules = submoduleParts.chunked(2).associate { (it[0] as String) to (it[1] as String) },
|
||||||
|
|||||||
@@ -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",
|
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"),
|
hints = listOf("Look up the 'git fetch' command"),
|
||||||
commandSuggestions = listOf("git fetch origin"),
|
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 = {
|
nativeSetup = {
|
||||||
resetFiles()
|
resetFiles()
|
||||||
write("master_file")
|
write("master_file")
|
||||||
@@ -33,9 +41,9 @@ internal fun fetchLevel(): Level = level(
|
|||||||
true
|
true
|
||||||
},
|
},
|
||||||
validator = repoPredicate { repo ->
|
validator = repoPredicate { repo ->
|
||||||
"fetch" in repo.maintenanceActions &&
|
repo.branches.size == 1 &&
|
||||||
"origin/new_branch" in repo.fetchedBranches &&
|
repo.fetchHeadCount == 2 &&
|
||||||
repo.headBranch == "master"
|
"pull" !in repo.maintenanceActions
|
||||||
},
|
},
|
||||||
testCases = listOf(
|
testCases = listOf(
|
||||||
levelTestCase("fetch origin", "git fetch origin"),
|
levelTestCase("fetch origin", "git fetch origin"),
|
||||||
|
|||||||
@@ -149,6 +149,8 @@ private fun RepoState.validationSnapshot(): String = buildString {
|
|||||||
append(remotes.toSortedMap())
|
append(remotes.toSortedMap())
|
||||||
append(", fetchedBranches=")
|
append(", fetchedBranches=")
|
||||||
append(fetchedBranches.sorted())
|
append(fetchedBranches.sorted())
|
||||||
|
append(", fetchHeadCount=")
|
||||||
|
append(fetchHeadCount)
|
||||||
append(", pushedBranches=")
|
append(", pushedBranches=")
|
||||||
append(pushedBranches.sorted())
|
append(pushedBranches.sorted())
|
||||||
append(", pushedTags=")
|
append(", pushedTags=")
|
||||||
|
|||||||
@@ -13,7 +13,29 @@ internal fun statusLevel(): Level = level(
|
|||||||
description = "Among the files in this repository, which of them is untracked?",
|
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."),
|
hints = listOf("You are looking for a command to identify the status of the repository."),
|
||||||
commandSuggestions = listOf("git status"),
|
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"),
|
validator = commandAnswer("database.yml"),
|
||||||
testCases = listOf(
|
testCases = listOf(
|
||||||
levelTestCase("answer untracked file", "database.yml"),
|
levelTestCase("answer untracked file", "database.yml"),
|
||||||
|
|||||||
@@ -101,7 +101,8 @@ class LevelSolutionsTest {
|
|||||||
@Test
|
@Test
|
||||||
fun reportedRegressionCommandsDoNotSolveLevels() {
|
fun reportedRegressionCommandsDoNotSolveLevels() {
|
||||||
val statusRepo = statusLevel().setup()
|
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 gitBinary = testGitBinary()
|
||||||
val runtimeRoot = testSandboxRoot().apply {
|
val runtimeRoot = testSandboxRoot().apply {
|
||||||
@@ -185,6 +186,7 @@ class LevelSolutionsTest {
|
|||||||
appendLine("config=${config.toSortedMap()}")
|
appendLine("config=${config.toSortedMap()}")
|
||||||
appendLine("stashes=$stashes")
|
appendLine("stashes=$stashes")
|
||||||
appendLine("fetchedBranches=${fetchedBranches.sorted()}")
|
appendLine("fetchedBranches=${fetchedBranches.sorted()}")
|
||||||
|
appendLine("fetchHeadCount=$fetchHeadCount")
|
||||||
appendLine("pushedBranches=${pushedBranches.sorted()}")
|
appendLine("pushedBranches=${pushedBranches.sorted()}")
|
||||||
appendLine("pushedTags=${pushedTags.sorted()}")
|
appendLine("pushedTags=${pushedTags.sorted()}")
|
||||||
appendLine("submodules=${submodules.toSortedMap()}")
|
appendLine("submodules=${submodules.toSortedMap()}")
|
||||||
|
|||||||
Reference in New Issue
Block a user