From 781b5090f7f6b08fba5914d8c8ebc6ea2bbf4a84 Mon Sep 17 00:00:00 2001 From: Joe Tretter Date: Fri, 26 Jun 2026 11:23:16 -0500 Subject: [PATCH] Fix Git editor reword workflow and UI coverage - show Git editor file paths instead of the submitted git command - surface follow-up commit message editors during interactive rebase reword - add rename-commit UI tests for editor and amend-message solution paths - add broader embedded level solution scenarios for alternate Git workflows --- app/build.gradle.kts | 4 +- .../GitRepositoryRuntimeInstrumentedTest.kt | 86 ++++++++++- .../githugandroid/GitEditorCommands.kt | 4 + .../githugandroid/GitEditorWorkflow.kt | 138 +++++++++++++++++- .../tretter/githugandroid/GitHugApp.kt | 10 +- .../githugandroid/GitMessageEditorDialog.kt | 25 +++- .../tretter/githugandroid/GitRuntime.kt | 10 ++ .../githugandroid/InteractiveAddEngine.kt | 1 + .../tretter/githugandroid/TextEditorDialog.kt | 2 +- .../tretter/githugandroid/levels/AddLevel.kt | 2 + .../githugandroid/levels/BisectLevel.kt | 8 + .../githugandroid/levels/BranchAtLevel.kt | 1 + .../githugandroid/levels/BranchLevel.kt | 1 + .../githugandroid/levels/CheckoutFileLevel.kt | 2 + .../githugandroid/levels/CheckoutLevel.kt | 2 + .../githugandroid/levels/CheckoutTagLevel.kt | 1 + .../levels/CheckoutTagOverBranchLevel.kt | 1 + .../githugandroid/levels/CommitAmendLevel.kt | 2 + .../levels/CommitInFutureLevel.kt | 1 + .../githugandroid/levels/CommitLevel.kt | 1 + .../githugandroid/levels/ConfigLevel.kt | 1 + .../githugandroid/levels/FetchLevel.kt | 1 + .../levels/FindOldBranchLevel.kt | 1 + .../tretter/githugandroid/levels/InitLevel.kt | 1 + .../githugandroid/levels/MergeLevel.kt | 1 + .../githugandroid/levels/MergeSquashLevel.kt | 1 + .../tretter/githugandroid/levels/PullLevel.kt | 1 + .../githugandroid/levels/PushBranchLevel.kt | 1 + .../tretter/githugandroid/levels/PushLevel.kt | 1 + .../githugandroid/levels/PushTagsLevel.kt | 1 + .../githugandroid/levels/RebaseLevel.kt | 1 + .../githugandroid/levels/RebaseOntoLevel.kt | 1 + .../githugandroid/levels/RemoteAddLevel.kt | 1 + .../githugandroid/levels/RenameCommitLevel.kt | 12 ++ .../githugandroid/levels/RenameLevel.kt | 1 + .../githugandroid/levels/ResetLevel.kt | 1 + .../githugandroid/levels/ResetSoftLevel.kt | 1 + .../githugandroid/levels/RestoreLevel.kt | 1 + .../githugandroid/levels/RestructureLevel.kt | 1 + .../githugandroid/levels/RevertLevel.kt | 2 + .../githugandroid/levels/RmCachedLevel.kt | 1 + .../tretter/githugandroid/levels/RmLevel.kt | 1 + .../githugandroid/levels/SquashLevel.kt | 6 + .../githugandroid/levels/StageLinesLevel.kt | 1 + .../githugandroid/levels/StashLevel.kt | 2 + .../githugandroid/levels/SubmoduleLevel.kt | 6 + .../tretter/githugandroid/levels/TagLevel.kt | 2 + .../githugandroid/GitEditorCommandsTest.kt | 3 + .../githugandroid/GitSandboxEngineTest.kt | 35 +++++ 49 files changed, 372 insertions(+), 19 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9603ede..467ae68 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,8 +20,8 @@ android { applicationId = "solutions.tretter.githugandroid" minSdk = 26 targetSdk = 35 - versionCode = 178 - versionName = "0.1.177" + versionCode = 179 + versionName = "0.1.178" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true diff --git a/app/src/androidTest/java/solutions/tretter/githugandroid/GitRepositoryRuntimeInstrumentedTest.kt b/app/src/androidTest/java/solutions/tretter/githugandroid/GitRepositoryRuntimeInstrumentedTest.kt index 5ffebc7..f4e1f72 100644 --- a/app/src/androidTest/java/solutions/tretter/githugandroid/GitRepositoryRuntimeInstrumentedTest.kt +++ b/app/src/androidTest/java/solutions/tretter/githugandroid/GitRepositoryRuntimeInstrumentedTest.kt @@ -4,11 +4,16 @@ import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performSemanticsAction import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performImeAction import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performTextClearance import androidx.compose.ui.test.performTextInput +import androidx.compose.ui.semantics.SemanticsActions +import androidx.compose.ui.semantics.SemanticsProperties +import androidx.compose.ui.semantics.getOrNull +import androidx.compose.ui.text.AnnotatedString import androidx.test.core.app.ActivityScenario import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry @@ -44,12 +49,41 @@ class GitRepositoryRuntimeInstrumentedTest { launchFreshApp().use { allGithugLevels().forEachIndexed { index, level -> waitForExercise(level) - level.testCases.first().commands.forEach(::submitTerminalCommand) + if (level.id == renameCommitLevel().id) { + completeRenameCommitWithEditor() + } else { + level.testCases.first().commands.forEach(::submitTerminalCommand) + } waitForLevelCompletion(nextLevel = allGithugLevels().getOrNull(index + 1)) } } } + @Test + fun renameCommitLevelCompletesThroughUiEditor() { + launchFreshApp().use { + selectLevel(renameCommitLevel()) + + completeRenameCommitWithEditor() + + waitForLevelCompletion(nextLevel = initLevel()) + } + } + + @Test + fun renameCommitLevelCompletesThroughMessageOptionAfterReword() { + launchFreshApp().use { + selectLevel(renameCommitLevel()) + + startRenameCommitReword() + composeRule.onNodeWithTag("git-message-editor-dismiss").performClick() + submitTerminalCommand("git commit --amend -m \"First commit\"") + submitTerminalCommand("git rebase --continue") + + waitForLevelCompletion(nextLevel = initLevel()) + } + } + @Test fun configLevelCompletesThroughUiWithArbitraryValues() { launchFreshApp().use { @@ -111,6 +145,56 @@ class GitRepositoryRuntimeInstrumentedTest { composeRule.onNodeWithTag("exercise-title-${nextLevel.id}").assertIsDisplayed() } + private fun selectLevel(level: Level) { + composeRule.onNodeWithTag("level-${level.id}") + .performScrollTo() + .performClick() + waitForExercise(level) + composeRule.waitUntil(timeoutMillis = 30_000) { + composeRule.onAllNodes(hasText("Loaded level: ${level.title}", substring = true)) + .fetchSemanticsNodes() + .isNotEmpty() + } + } + + private fun completeRenameCommitWithEditor() { + startRenameCommitReword() + waitForGitEditorPath(".git/COMMIT_EDITMSG") + replaceGitEditorContent( + currentGitEditorContent().replaceFirst("First coommit", "First commit"), + ) + composeRule.onNodeWithTag("git-message-editor-save").performClick() + } + + private fun startRenameCommitReword() { + submitTerminalCommand("git rebase -i HEAD~2") + waitForGitEditorPath(".git/rebase-merge/git-rebase-todo") + replaceGitEditorContent( + currentGitEditorContent().replaceFirst(Regex("(?m)^pick "), "reword "), + ) + composeRule.onNodeWithTag("git-message-editor-save").performClick() + } + + private fun waitForGitEditorPath(path: String) { + composeRule.waitUntil(timeoutMillis = 30_000) { + composeRule.onAllNodes(hasText(path, substring = true)).fetchSemanticsNodes().isNotEmpty() + } + composeRule.onNodeWithTag("git-message-editor-path").assertIsDisplayed() + } + + private fun currentGitEditorContent(): String { + val node = composeRule.onNodeWithTag("git-message-editor-content").fetchSemanticsNode() + return node.config.getOrNull(SemanticsProperties.EditableText)?.text + ?: error("Expected Git editor content semantics") + } + + private fun replaceGitEditorContent(content: String) { + composeRule.onNodeWithTag("git-message-editor-content") + .performSemanticsAction(SemanticsActions.SetText) { setText -> + setText(AnnotatedString(content)) + } + } + private fun submitTerminalCommand(command: String) { val input = composeRule.onNodeWithTag("terminal-command-input") input.performClick() diff --git a/app/src/main/java/solutions/tretter/githugandroid/GitEditorCommands.kt b/app/src/main/java/solutions/tretter/githugandroid/GitEditorCommands.kt index 9155add..ade6024 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/GitEditorCommands.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/GitEditorCommands.kt @@ -11,6 +11,7 @@ data class GitEditorInvocation( val command: String, val kind: GitEditorCommandKind, val title: String, + val displayPath: String, val initialContent: String = "", ) @@ -35,6 +36,7 @@ private fun parseGitCommitEditor(command: String, arguments: List): GitE command = command, kind = GitEditorCommandKind.COMMIT_MESSAGE, title = "Edit Commit Message", + displayPath = ".git/COMMIT_EDITMSG", ) } @@ -48,6 +50,7 @@ private fun parseGitRebaseEditor(command: String, arguments: List): GitE command = command, kind = GitEditorCommandKind.REBASE_TODO, title = "Edit Rebase Todo", + displayPath = ".git/rebase-merge/git-rebase-todo", ) } @@ -61,6 +64,7 @@ private fun parseGitTagEditor(command: String, arguments: List): GitEdit command = command, kind = GitEditorCommandKind.TAG_MESSAGE, title = "Edit Tag Message", + displayPath = ".git/TAG_EDITMSG", ) } diff --git a/app/src/main/java/solutions/tretter/githugandroid/GitEditorWorkflow.kt b/app/src/main/java/solutions/tretter/githugandroid/GitEditorWorkflow.kt index 24d1338..ac5426b 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/GitEditorWorkflow.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/GitEditorWorkflow.kt @@ -2,6 +2,17 @@ package solutions.tretter.githugandroid import java.io.File +data class GitEditorContinuation( + val invocation: GitEditorInvocation, + val content: String, +) + +data class GitEditorExecutionResult( + val repo: RepoState, + val outputLines: List, + val nextEditor: GitEditorContinuation? = null, +) + internal class GitEditorWorkflow( private val requireNativeGit: () -> File, private val prepareLevel: (Level) -> RepoState, @@ -57,7 +68,7 @@ internal class GitEditorWorkflow( currentRepo: RepoState, invocation: GitEditorInvocation, message: String, - ): Pair> { + ): GitEditorExecutionResult { val nativeGit = requireNativeGit() val (sandboxRoot, workingDir) = repositoryPaths(level, currentRepo) return when (invocation.kind) { @@ -83,7 +94,7 @@ internal class GitEditorWorkflow( ) GitEditorCommandKind.PATCH_HUNK -> { - currentRepo to listOf("Patch hunk editing is handled by Git, not the Android runtime.") + GitEditorExecutionResult(currentRepo, listOf("Patch hunk editing is handled by Git, not the Android runtime.")) } } } @@ -96,19 +107,39 @@ internal class GitEditorWorkflow( workingDir: File, invocation: GitEditorInvocation, message: String, - ): Pair> { + ): GitEditorExecutionResult { val messageFile = File(sandboxRoot, ".git/GITHUG_ANDROID_EDITMSG").apply { parentFile?.mkdirs() writeText(message) } - val arguments = GitSandboxEngine.tokenizeCommand(invocation.command) + val commandTokens = GitSandboxEngine.tokenizeCommand(invocation.command) + val arguments = commandTokens .drop(1) .toMutableList() .apply { addAll(listOf("-F", messageFile.absolutePath)) } val result = runGit(nativeGit, workingDir, arguments, emptyMap()) messageFile.delete() - return inspectSandbox(level, currentRepo.currentDir).copy(currentDir = currentRepo.currentDir) to result.outputLines + if ( + result.exitCode == 0 && + invocation.kind == GitEditorCommandKind.COMMIT_MESSAGE && + commandTokens.drop(1).isCommitAmendCommand() && + rebaseStateExists(File(sandboxRoot, ".git")) + ) { + return runRebaseContinueCapturingEditor( + level = level, + currentRepo = currentRepo, + nativeGit = nativeGit, + sandboxRoot = sandboxRoot, + workingDir = workingDir, + leadingOutput = result.outputLines, + ) + } + + return GitEditorExecutionResult( + repo = inspectSandbox(level, currentRepo.currentDir).copy(currentDir = currentRepo.currentDir), + outputLines = result.outputLines, + ) } private fun executeInteractiveRebase( @@ -119,7 +150,7 @@ internal class GitEditorWorkflow( workingDir: File, invocation: GitEditorInvocation, todo: String, - ): Pair> { + ): GitEditorExecutionResult { val gitDir = File(sandboxRoot, ".git").apply { mkdirs() } val todoFile = File(gitDir, "GITHUG_ANDROID_REBASE_TODO").apply { writeText(todo) @@ -132,19 +163,106 @@ internal class GitEditorWorkflow( |""".trimMargin(), ) } + val capture = editorCaptureFiles(gitDir) + capture.content.delete() + capture.path.delete() + val messageEditorScript = createCaptureEditorScript(gitDir, capture) val result = runGit( nativeGit, workingDir, GitSandboxEngine.tokenizeCommand(invocation.command).drop(1), mapOf( "GIT_SEQUENCE_EDITOR" to editorCommand(editorScript), - "GIT_EDITOR" to "true", + "GIT_EDITOR" to editorCommand(messageEditorScript), ), ) todoFile.delete() editorScript.delete() + messageEditorScript.delete() - return inspectSandbox(level, currentRepo.currentDir).copy(currentDir = currentRepo.currentDir) to result.outputLines + val nextEditor = capturedCommitMessageEditor(sandboxRoot, capture) + capture.content.delete() + capture.path.delete() + + return GitEditorExecutionResult( + repo = inspectSandbox(level, currentRepo.currentDir).copy(currentDir = currentRepo.currentDir), + outputLines = result.outputLines, + nextEditor = nextEditor, + ) + } + + private fun runRebaseContinueCapturingEditor( + level: Level, + currentRepo: RepoState, + nativeGit: File, + sandboxRoot: File, + workingDir: File, + leadingOutput: List, + ): GitEditorExecutionResult { + val gitDir = File(sandboxRoot, ".git").apply { mkdirs() } + val capture = editorCaptureFiles(gitDir) + capture.content.delete() + capture.path.delete() + val messageEditorScript = createCaptureEditorScript(gitDir, capture) + val result = runGit( + nativeGit, + workingDir, + listOf("rebase", "--continue"), + mapOf("GIT_EDITOR" to editorCommand(messageEditorScript)), + ) + messageEditorScript.delete() + + val nextEditor = capturedCommitMessageEditor(sandboxRoot, capture) + capture.content.delete() + capture.path.delete() + + return GitEditorExecutionResult( + repo = inspectSandbox(level, currentRepo.currentDir).copy(currentDir = currentRepo.currentDir), + outputLines = leadingOutput + result.outputLines, + nextEditor = nextEditor, + ) + } + + private data class EditorCaptureFiles( + val content: File, + val path: File, + ) + + private fun editorCaptureFiles(gitDir: File): EditorCaptureFiles = EditorCaptureFiles( + content = File(gitDir, "GITHUG_ANDROID_CAPTURED_EDITOR"), + path = File(gitDir, "GITHUG_ANDROID_CAPTURED_EDITOR_PATH"), + ) + + private fun createCaptureEditorScript(gitDir: File, capture: EditorCaptureFiles): File { + return File(gitDir, "githug-android-capture-message-editor.sh").apply { + writeText( + """ + |#!/bin/sh + |cat "$1" > ${capture.content.absolutePath.toShellSingleQuoted()} + |printf '%s\n' "$1" > ${capture.path.absolutePath.toShellSingleQuoted()} + |exit 1 + |""".trimMargin(), + ) + } + } + + private fun capturedCommitMessageEditor(sandboxRoot: File, capture: EditorCaptureFiles): GitEditorContinuation? { + val content = capture.content.takeIf { it.isFile }?.readText() ?: return null + val capturedPath = capture.path.takeIf { it.isFile }?.readText()?.trim().orEmpty() + val displayPath = capturedPath + .takeIf { it.isNotBlank() } + ?.let { File(it).relativeToOrSelf(sandboxRoot).path } + ?: ".git/COMMIT_EDITMSG" + return GitEditorContinuation( + invocation = GitEditorInvocation( + command = "git commit --amend", + kind = GitEditorCommandKind.COMMIT_MESSAGE, + title = "Edit Commit Message", + displayPath = displayPath, + initialContent = content, + ), + content = content, + ) } private fun repositoryPaths(level: Level, currentRepo: RepoState): Pair { @@ -166,6 +284,10 @@ internal class GitEditorWorkflow( return File(gitDir, "rebase-merge").exists() || File(gitDir, "rebase-apply").exists() } + private fun List.isCommitAmendCommand(): Boolean { + return firstOrNull() == "commit" && any { it == "--amend" } + } + private fun String.toShellSingleQuoted(): String { return "'" + replace("'", "'\"'\"'") + "'" } diff --git a/app/src/main/java/solutions/tretter/githugandroid/GitHugApp.kt b/app/src/main/java/solutions/tretter/githugandroid/GitHugApp.kt index 05b2715..bd0159f 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/GitHugApp.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/GitHugApp.kt @@ -513,14 +513,20 @@ fun GitHugApp() { fun saveGitMessageEditor() { val state = gitMessageEditorState ?: return - val (newRepo, lines) = runtime.executeGitEditorCommand( + val result = runtime.executeGitEditorCommandWithResult( level = currentLevel, currentRepo = repo, invocation = state.invocation, message = state.content, ) gitMessageEditorState = null - applyCommandResult(state.invocation.command, newRepo, lines, echoCommand = false) + applyCommandResult(state.invocation.command, result.repo, result.outputLines, echoCommand = false) + result.nextEditor?.let { nextEditor -> + gitMessageEditorState = GitMessageEditorState( + invocation = nextEditor.invocation, + content = nextEditor.content, + ) + } } fun runCommand() { diff --git a/app/src/main/java/solutions/tretter/githugandroid/GitMessageEditorDialog.kt b/app/src/main/java/solutions/tretter/githugandroid/GitMessageEditorDialog.kt index fe628fe..3fbd511 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/GitMessageEditorDialog.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/GitMessageEditorDialog.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight @@ -58,7 +59,7 @@ fun GitMessageEditorDialog( val keyboardController = LocalSoftwareKeyboardController.current LaunchedEffect(state.invocation.command) { - contentFocusRequester.requestFocus() + runCatching { contentFocusRequester.requestFocus() } keyboardController?.show() } @@ -86,6 +87,7 @@ fun GitMessageEditorDialog( ) { Text( text = state.invocation.title, + modifier = Modifier.testTag("git-message-editor-title"), color = TextPrimary, fontWeight = FontWeight.Bold, fontSize = 20.sp, @@ -94,11 +96,21 @@ fun GitMessageEditorDialog( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - EditorButton(label = "Save", enabled = state.content.isNotBlank(), onClick = onSave) - EditorButton(label = "Dismiss", onClick = onClose) + EditorButton( + label = "Save", + enabled = state.content.isNotBlank(), + modifier = Modifier.testTag("git-message-editor-save"), + onClick = onSave, + ) + EditorButton( + label = "Dismiss", + modifier = Modifier.testTag("git-message-editor-dismiss"), + onClick = onClose, + ) } Text( - text = state.invocation.command, + text = state.invocation.displayPath, + modifier = Modifier.testTag("git-message-editor-path"), color = TextSecondary, fontFamily = FontFamily.Monospace, fontSize = 13.sp, @@ -118,7 +130,8 @@ fun GitMessageEditorDialog( onValueChange = onContentChange, modifier = Modifier .sizeIn(minWidth = 1200.dp, minHeight = 1200.dp) - .focusRequester(contentFocusRequester), + .focusRequester(contentFocusRequester) + .testTag("git-message-editor-content"), textStyle = TextStyle( color = TextPrimary, fontFamily = FontFamily.Monospace, @@ -141,11 +154,13 @@ fun GitMessageEditorDialog( private fun EditorButton( label: String, enabled: Boolean = true, + modifier: Modifier = Modifier, onClick: () -> Unit, ) { Button( onClick = onClick, enabled = enabled, + modifier = modifier, colors = ButtonDefaults.buttonColors( containerColor = Accent, contentColor = AppBackground, diff --git a/app/src/main/java/solutions/tretter/githugandroid/GitRuntime.kt b/app/src/main/java/solutions/tretter/githugandroid/GitRuntime.kt index 4a6b89d..f035aa6 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/GitRuntime.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/GitRuntime.kt @@ -265,6 +265,16 @@ class GitRepositoryRuntime private constructor( invocation: GitEditorInvocation, message: String, ): Pair> { + val result = executeGitEditorCommandWithResult(level, currentRepo, invocation, message) + return result.repo to result.outputLines + } + + fun executeGitEditorCommandWithResult( + level: Level, + currentRepo: RepoState, + invocation: GitEditorInvocation, + message: String, + ): GitEditorExecutionResult { return editorWorkflow.execute(level, currentRepo, invocation, message) } diff --git a/app/src/main/java/solutions/tretter/githugandroid/InteractiveAddEngine.kt b/app/src/main/java/solutions/tretter/githugandroid/InteractiveAddEngine.kt index 342429f..208f5ec 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/InteractiveAddEngine.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/InteractiveAddEngine.kt @@ -51,6 +51,7 @@ internal object InteractiveAddEngine { command = command, kind = GitEditorCommandKind.PATCH_HUNK, title = "Edit Patch Hunk", + displayPath = target, initialContent = editablePatchHunkContent(file), ) } diff --git a/app/src/main/java/solutions/tretter/githugandroid/TextEditorDialog.kt b/app/src/main/java/solutions/tretter/githugandroid/TextEditorDialog.kt index 17d6bcf..0461ce3 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/TextEditorDialog.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/TextEditorDialog.kt @@ -65,7 +65,7 @@ fun TextEditorDialog( val keyboardController = LocalSoftwareKeyboardController.current LaunchedEffect(state.originalCommand, state.path) { - contentFocusRequester.requestFocus() + runCatching { contentFocusRequester.requestFocus() } keyboardController?.show() } diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/AddLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/AddLevel.kt index a89de52..cda8422 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/AddLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/AddLevel.kt @@ -18,5 +18,7 @@ internal fun addLevel(): Level = level( testCases = listOf( levelTestCase("add exact file", "git add README"), levelTestCase("add all", "git add ."), + levelTestCase("stage exact file", "git stage README"), + levelTestCase("add with path separator", "git add -- README"), ), ) diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/BisectLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/BisectLevel.kt index b05a430..07cf8ca 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/BisectLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/BisectLevel.kt @@ -91,6 +91,14 @@ internal fun bisectLevel(): Level = level( "git bisect run ./test-balance.sh", "c8c7c00", ), + levelTestCase( + "answer last good commit after shell run", + "git bisect start", + "git bisect bad HEAD", + "git bisect good known-good", + "git bisect run sh test-balance.sh", + "c8c7", + ), ), negativeTestCases = listOf( levelTestCase( diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/BranchAtLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/BranchAtLevel.kt index 2c2f541..b8acb04 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/BranchAtLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/BranchAtLevel.kt @@ -28,5 +28,6 @@ internal fun branchAtLevel(): Level = level( testCases = listOf( levelTestCase("branch at previous commit", "git branch test_branch HEAD~1"), levelTestCase("branch at caret", "git branch test_branch HEAD^"), + levelTestCase("branch at explicit master parent", "git branch test_branch master^"), ), ) diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/BranchLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/BranchLevel.kt index c5b0950..270ca9b 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/BranchLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/BranchLevel.kt @@ -17,5 +17,6 @@ internal fun branchLevel(): Level = level( validator = repoPredicate { repo -> "test_code" in repo.branches && repo.headBranch == "master" }, testCases = listOf( levelTestCase("create branch", "git branch test_code"), + levelTestCase("create branch from master", "git branch test_code master"), ), ) diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/CheckoutFileLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/CheckoutFileLevel.kt index 308a826..89a4876 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/CheckoutFileLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/CheckoutFileLevel.kt @@ -24,5 +24,7 @@ internal fun checkoutFileLevel(): Level = level( 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"), + levelTestCase("restore file from index", "git restore config.rb"), + levelTestCase("checkout file from explicit head", "git checkout HEAD -- config.rb"), ), ) diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/CheckoutLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/CheckoutLevel.kt index a11f0f1..eeb1d3c 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/CheckoutLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/CheckoutLevel.kt @@ -17,5 +17,7 @@ internal fun checkoutLevel(): Level = level( validator = repoPredicate { repo -> repo.headBranch == "my_branch" && "my_branch" in repo.branches }, testCases = listOf( levelTestCase("checkout new branch", "git checkout -b my_branch"), + levelTestCase("switch create branch", "git switch -c my_branch"), + levelTestCase("branch then checkout", "git branch my_branch", "git checkout my_branch"), ), ) diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/CheckoutTagLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/CheckoutTagLevel.kt index 9cada9a..cc26fee 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/CheckoutTagLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/CheckoutTagLevel.kt @@ -22,6 +22,7 @@ internal fun checkoutTagLevel(): Level = level( testCases = listOf( levelTestCase("checkout tag", "git checkout v1.2"), levelTestCase("checkout explicit tag", "git checkout tags/v1.2"), + levelTestCase("switch detached tag", "git switch --detach v1.2"), ), ) diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/CheckoutTagOverBranchLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/CheckoutTagOverBranchLevel.kt index 6691958..d975428 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/CheckoutTagOverBranchLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/CheckoutTagOverBranchLevel.kt @@ -26,5 +26,6 @@ internal fun checkoutTagOverBranchLevel(): Level = level( testCases = listOf( levelTestCase("checkout tag namespace", "git checkout tags/v1.2"), levelTestCase("checkout refs tag", "git checkout refs/tags/v1.2"), + levelTestCase("switch detached tag namespace", "git switch --detach tags/v1.2"), ), ) diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/CommitAmendLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/CommitAmendLevel.kt index 651af19..c8efa12 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/CommitAmendLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/CommitAmendLevel.kt @@ -17,5 +17,7 @@ internal fun commitAmendLevel(): Level = level( 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"), + levelTestCase("amend with message option", "git add forgotten_file.rb", "git commit --amend -m \"Initial commit\""), + levelTestCase("amend through editor", "git add forgotten_file.rb", "GIT_EDITOR=true git commit --amend"), ), ) diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/CommitInFutureLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/CommitInFutureLevel.kt index b768c60..e79d1d2 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/CommitInFutureLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/CommitInFutureLevel.kt @@ -22,6 +22,7 @@ internal fun commitInFutureLevel(): Level = level( }, testCases = listOf( levelTestCase("commit with date option", "git commit --date 2037-01-01T00:00:00+0000 -m \"Future commit\""), + levelTestCase("commit with author date environment", "GIT_AUTHOR_DATE=2037-01-01T00:00:00+0000 git commit -m \"Future commit\""), ), negativeTestCases = listOf( levelTestCase("current date commit does not solve", "git commit -m \"Current date commit\""), diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/CommitLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/CommitLevel.kt index d2cfb75..fbfb641 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/CommitLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/CommitLevel.kt @@ -18,5 +18,6 @@ internal fun commitLevel(): Level = level( testCases = listOf( levelTestCase("commit with message", "git commit -m \"Initial commit\""), levelTestCase("commit with alternate message", "git commit -m \"Add README\""), + levelTestCase("commit through editor", "GIT_EDITOR=\"sed -i '1iInitial commit'\" git commit"), ), ) diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/ConfigLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/ConfigLevel.kt index 572982d..d9a46be 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/ConfigLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/ConfigLevel.kt @@ -18,5 +18,6 @@ internal fun configLevel(): Level = level( 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"), + levelTestCase("explicit local config", "git config --local user.name GitHug", "git config --local user.email githug@example.com"), ), ) 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 00c53d9..b8435f3 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/FetchLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/FetchLevel.kt @@ -49,5 +49,6 @@ internal fun fetchLevel(): Level = level( testCases = listOf( levelTestCase("fetch origin", "git fetch origin"), levelTestCase("fetch default", "git fetch"), + levelTestCase("fetch all remotes", "git fetch --all"), ), ) diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/FindOldBranchLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/FindOldBranchLevel.kt index d34b4e2..abd666c 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/FindOldBranchLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/FindOldBranchLevel.kt @@ -46,6 +46,7 @@ internal fun findOldBranchLevel(): Level = level( validator = repoPredicate { repo -> repo.headBranch == "solve_world_hunger" }, testCases = listOf( levelTestCase("checkout old branch", "git checkout solve_world_hunger"), + levelTestCase("switch old branch", "git switch solve_world_hunger"), ), setupChecks = listOf( levelSetupCheck( diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/InitLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/InitLevel.kt index ed6e923..3c17d6e 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/InitLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/InitLevel.kt @@ -17,6 +17,7 @@ internal fun initLevel(): Level = level( validator = repoPredicate { it.initialized }, testCases = listOf( levelTestCase("plain init", "git init"), + levelTestCase("init current directory explicitly", "git init ."), ), setupChecks = listOf( levelSetupCheck( diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/MergeLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/MergeLevel.kt index 190cebf..7c6a5f5 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/MergeLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/MergeLevel.kt @@ -30,6 +30,7 @@ internal fun mergeLevel(): Level = level( }, testCases = listOf( levelTestCase("merge feature", "git merge feature"), + levelTestCase("merge feature with explicit merge commit", "git merge --no-ff feature -m \"Merge feature\""), ), negativeTestCases = listOf( levelTestCase("switching to feature does not solve", "git switch feature"), diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/MergeSquashLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/MergeSquashLevel.kt index 74174ba..b24c2ec 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/MergeSquashLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/MergeSquashLevel.kt @@ -37,5 +37,6 @@ internal fun mergeSquashLevel(): Level = level( }, testCases = listOf( levelTestCase("squash merge then commit", "git merge --squash long-feature-branch", "git commit -m \"Merge long feature\""), + levelTestCase("squash no commit then commit", "git merge --squash --no-commit long-feature-branch", "git commit -m \"Merge long feature\""), ), ) diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/PullLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/PullLevel.kt index 89a0aec..64f2cf5 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/PullLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/PullLevel.kt @@ -40,5 +40,6 @@ internal fun pullLevel(): Level = level( testCases = listOf( levelTestCase("pull explicit remote branch", "git pull origin master"), levelTestCase("pull default origin", "git pull"), + levelTestCase("fetch then merge", "git fetch origin", "git merge origin/master"), ), ) diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/PushBranchLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/PushBranchLevel.kt index 5a53bc6..24032e1 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/PushBranchLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/PushBranchLevel.kt @@ -45,5 +45,6 @@ internal fun pushBranchLevel(): Level = level( testCases = listOf( levelTestCase("push named branch", "git push origin test_branch"), levelTestCase("push current branch refspec", "git push origin test_branch:test_branch"), + levelTestCase("push fully qualified branch refspec", "git push origin refs/heads/test_branch:refs/heads/test_branch"), ), ) diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/PushLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/PushLevel.kt index 8abda26..a13246c 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/PushLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/PushLevel.kt @@ -86,5 +86,6 @@ internal fun pushLevel(): Level = level( 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"), + levelTestCase("fetch rebase upstream push", "git fetch", "git rebase origin/master", "git push"), ), ) diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/PushTagsLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/PushTagsLevel.kt index 42c9f71..6bcee74 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/PushTagsLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/PushTagsLevel.kt @@ -35,5 +35,6 @@ internal fun pushTagsLevel(): Level = level( testCases = listOf( levelTestCase("push all tags", "git push --tags"), levelTestCase("push tags to origin", "git push origin --tags"), + levelTestCase("push named tag", "git push origin tag tag_to_be_pushed"), ), ) diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/RebaseLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/RebaseLevel.kt index e5aa837..5763317 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/RebaseLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/RebaseLevel.kt @@ -33,5 +33,6 @@ internal fun rebaseLevel(): Level = level( testCases = listOf( levelTestCase("checkout feature then rebase", "git checkout feature", "git rebase master"), levelTestCase("rebase named branch", "git rebase master feature"), + levelTestCase("switch feature then rebase", "git switch feature", "git rebase master"), ), ) diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/RebaseOntoLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/RebaseOntoLevel.kt index cd0ea57..c22312e 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/RebaseOntoLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/RebaseOntoLevel.kt @@ -44,5 +44,6 @@ internal fun rebaseOntoLevel(): Level = level( 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"), + levelTestCase("rebase onto full branch ref", "git rebase --onto refs/heads/master wrong_branch readme-update"), ), ) diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/RemoteAddLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/RemoteAddLevel.kt index 22d5181..36e88d7 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/RemoteAddLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/RemoteAddLevel.kt @@ -17,5 +17,6 @@ internal fun remoteAddLevel(): Level = level( 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"), + levelTestCase("add origin remote with tracked branch option", "git remote add -t master origin https://github.com/githug/githug"), ), ) diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/RenameCommitLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/RenameCommitLevel.kt index 6e689dc..7edb21d 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/RenameCommitLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/RenameCommitLevel.kt @@ -30,5 +30,17 @@ internal fun renameCommitLevel(): Level = level( "interactive rebase rename", "GIT_SEQUENCE_EDITOR=\"sed -i '1s/^pick /reword /'\" GIT_EDITOR=\"sed -i '1s/First coommit/First commit/'\" git rebase -i HEAD~2", ), + levelTestCase( + "interactive rebase then amend message option", + "GIT_SEQUENCE_EDITOR=\"sed -i '1s/^pick /reword /'\" GIT_EDITOR=false git rebase -i HEAD~2", + "git commit --amend -m \"First commit\"", + "git rebase --continue", + ), + levelTestCase( + "interactive rebase then amend editor", + "GIT_SEQUENCE_EDITOR=\"sed -i '1s/^pick /reword /'\" GIT_EDITOR=false git rebase -i HEAD~2", + "GIT_EDITOR=\"sed -i '1s/First coommit/First commit/'\" git commit --amend", + "git rebase --continue", + ), ), ) diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/RenameLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/RenameLevel.kt index 58d2e46..6954cdc 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/RenameLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/RenameLevel.kt @@ -17,5 +17,6 @@ internal fun renameLevel(): Level = level( validator = repoPredicate { repo -> repo.files.any { it.name == "newfile.txt" } && repo.files.none { it.name == "oldfile.txt" && !it.deleted } }, testCases = listOf( levelTestCase("git mv", "git mv oldfile.txt newfile.txt"), + levelTestCase("git mv force", "git mv -f oldfile.txt newfile.txt"), ), ) diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/ResetLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/ResetLevel.kt index 07bfefa..456f8e3 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/ResetLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/ResetLevel.kt @@ -26,5 +26,6 @@ internal fun resetLevel(): Level = level( 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"), + levelTestCase("restore staged path", "git restore --staged to_commit_second.rb"), ), ) diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/ResetSoftLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/ResetSoftLevel.kt index 6018755..d676681 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/ResetSoftLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/ResetSoftLevel.kt @@ -25,5 +25,6 @@ internal fun resetSoftLevel(): Level = level( 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^"), + levelTestCase("soft reset previous commit", "git reset --soft HEAD~1"), ), ) diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/RestoreLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/RestoreLevel.kt index c340389..97bdeac 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/RestoreLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/RestoreLevel.kt @@ -28,5 +28,6 @@ internal fun restoreLevel(): Level = level( validator = repoPredicate { repo -> repo.files.any { it.name == "file3" && it.tracked } }, testCases = listOf( levelTestCase("checkout file from reflog commit", "git checkout HEAD@{1} -- file3"), + levelTestCase("restore file from reflog commit", "git restore --source=HEAD@{1} --staged --worktree file3"), ), ) diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/RestructureLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/RestructureLevel.kt index 2ee6237..b011286 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/RestructureLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/RestructureLevel.kt @@ -18,5 +18,6 @@ internal fun restructureLevel(): Level = level( 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"), + levelTestCase("move with wildcard into slash directory", "mkdir src", "git mv *.html src/"), ), ) diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/RevertLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/RevertLevel.kt index 0a6d11c..2cd5031 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/RevertLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/RevertLevel.kt @@ -27,5 +27,7 @@ internal fun revertLevel(): Level = level( validator = repoPredicate { repo -> repo.commits.any { it.message.startsWith("Revert") } }, testCases = listOf( levelTestCase("revert middle commit", "git revert HEAD~1"), + levelTestCase("revert middle commit without editor", "git revert --no-edit HEAD~1"), + levelTestCase("revert caret commit", "git revert HEAD^"), ), ) diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/RmCachedLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/RmCachedLevel.kt index 166a1e2..8ee4e1f 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/RmCachedLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/RmCachedLevel.kt @@ -17,5 +17,6 @@ internal fun rmCachedLevel(): Level = level( 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"), + levelTestCase("reset staged path", "git reset deleteme.rb"), ), ) diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/RmLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/RmLevel.kt index f197232..f24ded2 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/RmLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/RmLevel.kt @@ -20,5 +20,6 @@ internal fun rmLevel(): Level = level( }, testCases = listOf( levelTestCase("git rm deleted path", "git rm deleteme.rb"), + levelTestCase("stage deleted path", "git add -u deleteme.rb"), ), ) diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/SquashLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/SquashLevel.kt index 9a0c1d7..4e5ee8d 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/SquashLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/SquashLevel.kt @@ -39,6 +39,12 @@ internal fun squashLevel(): Level = level( "git reset --soft HEAD~4", "git commit -m \"New commit message\"", ), + levelTestCase( + "mixed reset add and recommit", + "git reset HEAD~4", + "git add README", + "git commit -m \"New commit message\"", + ), ), negativeTestCases = listOf( levelTestCase("reset without replacement commit", "git reset --soft HEAD~4"), diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/StageLinesLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/StageLinesLevel.kt index 31b590b..64f72a6 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/StageLinesLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/StageLinesLevel.kt @@ -24,5 +24,6 @@ internal fun stageLinesLevel(): Level = level( validator = repoPredicate { repo -> repo.files.any { it.name == "feature.rb" && it.staged } }, testCases = listOf( levelTestCase("stage feature file", "git add feature.rb"), + levelTestCase("stage alias", "git stage feature.rb"), ), ) diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/StashLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/StashLevel.kt index 77cc922..c2fafed 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/StashLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/StashLevel.kt @@ -24,6 +24,8 @@ internal fun stashLevel(): Level = level( validator = repoPredicate { repo -> repo.stashes.isNotEmpty() && repo.files.none { it.staged } }, testCases = listOf( levelTestCase("stash changes", "git stash"), + levelTestCase("stash push", "git stash push"), + levelTestCase("stash with message", "git stash push -m \"save lyrics\""), ), setupChecks = listOf( levelSetupCheck( diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/SubmoduleLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/SubmoduleLevel.kt index d9a7170..95de3d1 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/SubmoduleLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/SubmoduleLevel.kt @@ -40,5 +40,11 @@ internal fun submoduleLevel(): Level = level( "git config -f .gitmodules submodule.githug-include-me.url ../submodule-source", "git add .gitmodules", ), + levelTestCase( + "record submodule metadata url first", + "git config -f .gitmodules submodule.githug-include-me.url ../submodule-source", + "git config -f .gitmodules submodule.githug-include-me.path githug-include-me", + "git add .gitmodules", + ), ), ) diff --git a/app/src/main/java/solutions/tretter/githugandroid/levels/TagLevel.kt b/app/src/main/java/solutions/tretter/githugandroid/levels/TagLevel.kt index d459610..bce3900 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/levels/TagLevel.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/levels/TagLevel.kt @@ -17,5 +17,7 @@ internal fun tagLevel(): Level = level( validator = repoPredicate { repo -> "new_tag" in repo.tags }, testCases = listOf( levelTestCase("create tag", "git tag new_tag"), + levelTestCase("create tag at head", "git tag new_tag HEAD"), + levelTestCase("create annotated tag", "git tag -a new_tag -m \"New tag\""), ), ) diff --git a/app/src/test/java/solutions/tretter/githugandroid/GitEditorCommandsTest.kt b/app/src/test/java/solutions/tretter/githugandroid/GitEditorCommandsTest.kt index 67cc437..e18eba9 100644 --- a/app/src/test/java/solutions/tretter/githugandroid/GitEditorCommandsTest.kt +++ b/app/src/test/java/solutions/tretter/githugandroid/GitEditorCommandsTest.kt @@ -11,6 +11,7 @@ class GitEditorCommandsTest { assertEquals(GitEditorCommandKind.COMMIT_MESSAGE, invocation?.kind) assertEquals("Edit Commit Message", invocation?.title) + assertEquals(".git/COMMIT_EDITMSG", invocation?.displayPath) } @Test @@ -34,6 +35,7 @@ class GitEditorCommandsTest { assertEquals(GitEditorCommandKind.REBASE_TODO, invocation?.kind) assertEquals("Edit Rebase Todo", invocation?.title) + assertEquals(".git/rebase-merge/git-rebase-todo", invocation?.displayPath) } @Test @@ -47,6 +49,7 @@ class GitEditorCommandsTest { assertEquals(GitEditorCommandKind.TAG_MESSAGE, invocation?.kind) assertEquals("Edit Tag Message", invocation?.title) + assertEquals(".git/TAG_EDITMSG", invocation?.displayPath) } @Test diff --git a/app/src/test/java/solutions/tretter/githugandroid/GitSandboxEngineTest.kt b/app/src/test/java/solutions/tretter/githugandroid/GitSandboxEngineTest.kt index 6720b2c..1fb3069 100644 --- a/app/src/test/java/solutions/tretter/githugandroid/GitSandboxEngineTest.kt +++ b/app/src/test/java/solutions/tretter/githugandroid/GitSandboxEngineTest.kt @@ -2,6 +2,8 @@ package solutions.tretter.githugandroid import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Assume.assumeTrue import org.junit.Test @@ -451,6 +453,39 @@ class GitSandboxEngineTest { } } + @Test + fun nativeInteractiveRebaseRewordRequestsCommitMessageEditorAndContinues() { + val git = testGitBinary() + assumeTrue(git.exists() && git.canExecute()) + val root = Files.createTempDirectory("githug-interactive-rebase-reword-editor").toFile() + try { + val runtime = GitRepositoryRuntime(root, git) + val level = renameCommitLevel() + val repo = runtime.prepareLevel(level) + val invocation = parseGitEditorInvocation("git rebase -i HEAD~2") + ?: error("Expected interactive rebase editor invocation") + val todo = runtime.gitEditorInitialContent(level, repo, invocation) + val rewordTodo = todo.replaceFirst(Regex("(?m)^pick "), "reword ") + + val rebaseResult = runtime.executeGitEditorCommandWithResult(level, repo, invocation, rewordTodo) + val nextEditor = rebaseResult.nextEditor + assertNotNull(nextEditor) + val message = nextEditor!!.content.replaceFirst("First coommit", "First commit") + val completedResult = runtime.executeGitEditorCommandWithResult( + level = level, + currentRepo = rebaseResult.repo, + invocation = nextEditor.invocation, + message = message, + ) + + assertEquals(".git/COMMIT_EDITMSG", nextEditor.invocation.displayPath) + assertNull(completedResult.nextEditor) + assertTrue(level.validator(completedResult.repo, nextEditor.invocation.command)) + } finally { + root.deleteRecursively() + } + } + @Test fun nativeExecutableShortcutRunsScriptThroughShell() { val git = testGitBinary()