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
This commit is contained in:
@@ -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<String>): 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<String>): 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<String>): GitEdit
|
||||
command = command,
|
||||
kind = GitEditorCommandKind.TAG_MESSAGE,
|
||||
title = "Edit Tag Message",
|
||||
displayPath = ".git/TAG_EDITMSG",
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String>,
|
||||
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<RepoState, List<String>> {
|
||||
): 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<RepoState, List<String>> {
|
||||
): 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<RepoState, List<String>> {
|
||||
): 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<String>,
|
||||
): 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<File, File> {
|
||||
@@ -166,6 +284,10 @@ internal class GitEditorWorkflow(
|
||||
return File(gitDir, "rebase-merge").exists() || File(gitDir, "rebase-apply").exists()
|
||||
}
|
||||
|
||||
private fun List<String>.isCommitAmendCommand(): Boolean {
|
||||
return firstOrNull() == "commit" && any { it == "--amend" }
|
||||
}
|
||||
|
||||
private fun String.toShellSingleQuoted(): String {
|
||||
return "'" + replace("'", "'\"'\"'") + "'"
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -265,6 +265,16 @@ class GitRepositoryRuntime private constructor(
|
||||
invocation: GitEditorInvocation,
|
||||
message: String,
|
||||
): Pair<RepoState, List<String>> {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ internal object InteractiveAddEngine {
|
||||
command = command,
|
||||
kind = GitEditorCommandKind.PATCH_HUNK,
|
||||
title = "Edit Patch Hunk",
|
||||
displayPath = target,
|
||||
initialContent = editablePatchHunkContent(file),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ fun TextEditorDialog(
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
LaunchedEffect(state.originalCommand, state.path) {
|
||||
contentFocusRequester.requestFocus()
|
||||
runCatching { contentFocusRequester.requestFocus() }
|
||||
keyboardController?.show()
|
||||
}
|
||||
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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^"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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\""),
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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\""),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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/"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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^"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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\""),
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user