diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6919f08..a84c6f2 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 = 154 - versionName = "0.1.153" + versionCode = 155 + versionName = "0.1.154" 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 18fa6cc..fd89455 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/GameModels.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/GameModels.kt @@ -23,6 +23,8 @@ data class CommitNode( data class InteractiveAddSession( val target: String? = null, val awaitingUpdateSelection: Boolean = false, + val selectionPrompt: String = "Update>>", + val selectionAction: String = "update", ) data class RepoState( diff --git a/app/src/main/java/solutions/tretter/githugandroid/GitSandboxEngine.kt b/app/src/main/java/solutions/tretter/githugandroid/GitSandboxEngine.kt index 222e9ed..9e35629 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/GitSandboxEngine.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/GitSandboxEngine.kt @@ -209,9 +209,11 @@ object GitSandboxEngine { } else { when (answer.lowercase()) { "1", "s", "status" -> repo to listOf("What now> $answer") + interactiveAddConsoleLines(interactiveAddCandidates(repo, session.target)) - "2", "u", "update" -> repo.copy( - interactiveAddSession = session.copy(awaitingUpdateSelection = true), - ) to listOf("What now> $answer", "Update>>") + "2", "u", "update" -> interactiveAddSelectionPrompt(repo, session, answer, "Update>>", "update") + "3", "r", "revert" -> interactiveAddSelectionPrompt(repo, session, answer, "Revert>>", "revert") + "4", "a", "add untracked", "add-untracked" -> interactiveAddSelectionPrompt(repo, session, answer, "Add untracked>>", "add-untracked") + "5", "p", "patch" -> interactiveAddSelectionPrompt(repo, session, answer, "Patch update>>", "patch") + "6", "d", "diff" -> interactiveAddSelectionPrompt(repo, session, answer, "Diff>>", "diff") "7", "q", "quit" -> repo.copy(interactiveAddSession = null) to listOf("What now> $answer", "Bye.") "8", "h", "help" -> repo to listOf("What now> $answer") + interactiveAddConsoleLines(interactiveAddCandidates(repo, session.target)) else -> repo to listOf("What now> $answer", "Huh ($answer)?") + interactiveAddConsoleLines(interactiveAddCandidates(repo, session.target)) @@ -219,6 +221,22 @@ object GitSandboxEngine { } } + private fun interactiveAddSelectionPrompt( + repo: RepoState, + session: InteractiveAddSession, + answer: String, + prompt: String, + action: String, + ): Pair> { + return repo.copy( + interactiveAddSession = session.copy( + awaitingUpdateSelection = true, + selectionPrompt = prompt, + selectionAction = action, + ), + ) to listOf("What now> $answer", prompt) + } + private fun applyInteractiveAddUpdateSelection( repo: RepoState, session: InteractiveAddSession, @@ -226,27 +244,64 @@ object GitSandboxEngine { ): Pair> { val candidates = interactiveAddCandidates(repo, session.target) val selectedNames = selectedInteractiveAddNames(candidates, answer) + val prompt = session.selectionPrompt if (selectedNames.isEmpty()) { - return repo to listOf("Update>> $answer", "No files selected.", "Update>>") + return repo to listOf("$prompt $answer", "No files selected.", prompt) } - val updatedFiles = repo.files.map { file -> - if (file.name in selectedNames && !file.deleted) file.copy(staged = true) else file - } + val updatedFiles = applyInteractiveAddSelectionAction(repo, selectedNames, session.selectionAction) val updatedRepo = repo.copy( files = updatedFiles, - interactiveAddSession = session.copy(awaitingUpdateSelection = false), + interactiveAddSession = session.copy( + awaitingUpdateSelection = false, + selectionPrompt = "Update>>", + selectionAction = "update", + ), ) - val stagedCount = updatedFiles.count { updatedFile -> - val before = repo.files.firstOrNull { it.name == updatedFile.name } - updatedFile.staged && before?.staged != true - } + val summary = interactiveAddSelectionSummary(repo, updatedFiles, selectedNames, session.selectionAction) return updatedRepo to listOf( - "Update>> $answer", - "updated $stagedCount path(s)", + "$prompt $answer", + summary, ) + interactiveAddConsoleLines(interactiveAddCandidates(updatedRepo, session.target)) } + private fun applyInteractiveAddSelectionAction(repo: RepoState, selectedNames: Set, action: String): List { + return when (action) { + "revert" -> repo.files.mapNotNull { file -> + if (file.name !in selectedNames || file.deleted) { + file + } else if (file.tracked) { + file.copy(content = "", staged = false, deleted = false) + } else { + null + } + } + "diff" -> repo.files + else -> repo.files.map { file -> + if (file.name in selectedNames && !file.deleted) file.copy(staged = true) else file + } + } + } + + private fun interactiveAddSelectionSummary( + repo: RepoState, + updatedFiles: List, + selectedNames: Set, + action: String, + ): String { + return when (action) { + "revert" -> "reverted ${selectedNames.size} path(s)" + "diff" -> selectedNames.joinToString("\n") { "diff -- $it" } + else -> { + val stagedCount = updatedFiles.count { updatedFile -> + val before = repo.files.firstOrNull { it.name == updatedFile.name } + updatedFile.staged && before?.staged != true + } + "updated $stagedCount path(s)" + } + } + } + private fun interactiveAddCandidates(repo: RepoState, target: String?): List { return repo.files.filter { file -> !file.deleted && (target == null || target == "." || file.name == target || file.name.startsWith(target.trimEnd('/') + "/")) diff --git a/app/src/main/java/solutions/tretter/githugandroid/RepoStateSaver.kt b/app/src/main/java/solutions/tretter/githugandroid/RepoStateSaver.kt index 2fad9aa..9a6279d 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/RepoStateSaver.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/RepoStateSaver.kt @@ -21,7 +21,14 @@ val RepoStateSaver = listSaver( state.submodules.flatMap { listOf(it.key, it.value) }, state.maintenanceActions.toList(), state.fetchHeadCount, - state.interactiveAddSession?.let { listOf(it.target.orEmpty(), it.awaitingUpdateSelection.toString()) }.orEmpty(), + state.interactiveAddSession?.let { + listOf( + it.target.orEmpty(), + it.awaitingUpdateSelection.toString(), + it.selectionPrompt, + it.selectionAction, + ) + }.orEmpty(), ) }, restore = { saved -> @@ -70,6 +77,8 @@ val RepoStateSaver = listSaver( InteractiveAddSession( target = (it[0] as String).ifBlank { null }, awaitingUpdateSelection = (it[1] as String).toBoolean(), + selectionPrompt = it.getOrNull(2) as? String ?: "Update>>", + selectionAction = it.getOrNull(3) as? String ?: "update", ) }, ) diff --git a/app/src/test/java/solutions/tretter/githugandroid/GitSandboxEngineTest.kt b/app/src/test/java/solutions/tretter/githugandroid/GitSandboxEngineTest.kt index 88a9fe2..c46e807 100644 --- a/app/src/test/java/solutions/tretter/githugandroid/GitSandboxEngineTest.kt +++ b/app/src/test/java/solutions/tretter/githugandroid/GitSandboxEngineTest.kt @@ -369,6 +369,73 @@ class GitSandboxEngineTest { assertTrue(quitOutput.any { it.contains("Bye.") }) } + @Test + fun interactiveAddHandlesEveryDisplayedMenuCommand() { + val menuCommands = listOf( + "1" to "What now> 1", + "2" to "Update>>", + "3" to "Revert>>", + "4" to "Add untracked>>", + "5" to "Patch update>>", + "6" to "Diff>>", + "7" to "Bye.", + "8" to "What now> 8", + ) + + menuCommands.forEach { (command, expectedOutput) -> + val repo = RepoState(initialized = true, files = listOf(GitFile("README"))) + val (menuRepo, _) = GitSandboxEngine.execute(repo, "git add -i") + val (updatedRepo, output) = GitSandboxEngine.execute(menuRepo, command) + + assertFalse("$command should not be rejected", output.any { it.contains("Huh ($command)?") }) + assertTrue("$command should produce $expectedOutput", output.any { it.contains(expectedOutput) }) + if (command in listOf("2", "3", "4", "5", "6")) { + assertTrue(updatedRepo.interactiveAddSession?.awaitingUpdateSelection == true) + } + } + } + + @Test + fun interactiveAddHandlesMenuCommandAliases() { + val aliases = listOf( + "status" to "What now> status", + "update" to "Update>>", + "revert" to "Revert>>", + "add untracked" to "Add untracked>>", + "patch" to "Patch update>>", + "diff" to "Diff>>", + "quit" to "Bye.", + "help" to "What now> help", + ) + + aliases.forEach { (command, expectedOutput) -> + val repo = RepoState(initialized = true, files = listOf(GitFile("README"))) + val (menuRepo, _) = GitSandboxEngine.execute(repo, "git add -i") + val (updatedRepo, output) = GitSandboxEngine.execute(menuRepo, command) + + assertFalse("$command should not be rejected", output.any { it.contains("Huh ($command)?") }) + assertTrue("$command should produce $expectedOutput", output.any { it.contains(expectedOutput) }) + if (command in listOf("update", "revert", "add untracked", "patch", "diff")) { + assertTrue(updatedRepo.interactiveAddSession?.awaitingUpdateSelection == true) + } + } + } + + @Test + fun interactiveAddPatchSelectionStagesSelectedPath() { + val repo = RepoState(initialized = true, files = listOf(GitFile("README"))) + + val (menuRepo, _) = GitSandboxEngine.execute(repo, "git add -i") + val (patchRepo, patchOutput) = GitSandboxEngine.execute(menuRepo, "patch") + val (selectedRepo, selectionOutput) = GitSandboxEngine.execute(patchRepo, "1") + + assertTrue(patchRepo.interactiveAddSession?.awaitingUpdateSelection == true) + assertTrue(patchOutput.any { it.contains("Patch update>>") }) + assertTrue(selectedRepo.files.single { it.name == "README" }.staged) + assertTrue(selectionOutput.any { it.contains("Patch update>> 1") }) + assertTrue(selectionOutput.any { it.contains("updated 1 path(s)") }) + } + @Test fun nativeInteractiveAddSelectionUpdatesGitIndex() { val git = testGitBinary()