Open patch hunk editor for git add -p edit command
This commit is contained in:
@@ -19,8 +19,8 @@ android {
|
|||||||
applicationId = "solutions.tretter.githugandroid"
|
applicationId = "solutions.tretter.githugandroid"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 156
|
versionCode = 157
|
||||||
versionName = "0.1.155"
|
versionName = "0.1.156"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ enum class GitEditorCommandKind {
|
|||||||
COMMIT_MESSAGE,
|
COMMIT_MESSAGE,
|
||||||
REBASE_TODO,
|
REBASE_TODO,
|
||||||
TAG_MESSAGE,
|
TAG_MESSAGE,
|
||||||
|
PATCH_HUNK,
|
||||||
}
|
}
|
||||||
|
|
||||||
data class GitEditorInvocation(
|
data class GitEditorInvocation(
|
||||||
|
|||||||
@@ -405,10 +405,15 @@ fun GitHugApp() {
|
|||||||
|
|
||||||
fun openGitMessageEditor(invocation: GitEditorInvocation) {
|
fun openGitMessageEditor(invocation: GitEditorInvocation) {
|
||||||
val initialContent = runtime.gitEditorInitialContent(currentLevel, repo, invocation)
|
val initialContent = runtime.gitEditorInitialContent(currentLevel, repo, invocation)
|
||||||
|
val openedMessage = when (invocation.kind) {
|
||||||
|
GitEditorCommandKind.REBASE_TODO -> "Opened Git rebase editor"
|
||||||
|
GitEditorCommandKind.PATCH_HUNK -> "Opened Git patch editor"
|
||||||
|
else -> "Opened Git message editor"
|
||||||
|
}
|
||||||
output = buildList {
|
output = buildList {
|
||||||
addAll(output)
|
addAll(output)
|
||||||
add("$ ${invocation.command}")
|
add("$ ${invocation.command}")
|
||||||
add(if (invocation.kind == GitEditorCommandKind.REBASE_TODO) "Opened Git rebase editor" else "Opened Git message editor")
|
add(openedMessage)
|
||||||
}
|
}
|
||||||
gitMessageEditorState = GitMessageEditorState(
|
gitMessageEditorState = GitMessageEditorState(
|
||||||
invocation = invocation.copy(initialContent = initialContent),
|
invocation = invocation.copy(initialContent = initialContent),
|
||||||
@@ -474,6 +479,12 @@ fun GitHugApp() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val patchHunkEditorInvocation = GitSandboxEngine.parsePatchHunkEditorInvocation(repo, raw)
|
||||||
|
if (patchHunkEditorInvocation != null) {
|
||||||
|
openGitMessageEditor(patchHunkEditorInvocation)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val (newRepo, lines) = runtime.execute(currentLevel, repo, raw)
|
val (newRepo, lines) = runtime.execute(currentLevel, repo, raw)
|
||||||
applyCommandResult(raw, newRepo, lines, echoCommand = !isInteractiveInput)
|
applyCommandResult(raw, newRepo, lines, echoCommand = !isInteractiveInput)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -315,6 +315,21 @@ class GitRepositoryRuntime private constructor(
|
|||||||
return executeInteractiveRebaseEditorCommand(level, currentRepo, nativeGit, sandboxRoot, workingDir, invocation, message)
|
return executeInteractiveRebaseEditorCommand(level, currentRepo, nativeGit, sandboxRoot, workingDir, invocation, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (invocation.kind == GitEditorCommandKind.PATCH_HUNK) {
|
||||||
|
val (updatedRepo, output) = GitSandboxEngine.applyPatchHunkEdit(currentRepo, message)
|
||||||
|
val newlyStagedPaths = updatedRepo.files.filter { updatedFile ->
|
||||||
|
updatedFile.staged && currentRepo.files.firstOrNull { it.name == updatedFile.name }?.staged != true
|
||||||
|
}.map { it.name }
|
||||||
|
if (newlyStagedPaths.isNotEmpty()) {
|
||||||
|
runGit(nativeGit, workingDir, listOf("add") + newlyStagedPaths)
|
||||||
|
}
|
||||||
|
val inspectedRepo = inspectSandbox(level).copy(
|
||||||
|
currentDir = updatedRepo.currentDir,
|
||||||
|
interactiveAddSession = updatedRepo.interactiveAddSession,
|
||||||
|
)
|
||||||
|
return inspectedRepo to output
|
||||||
|
}
|
||||||
|
|
||||||
val messageFile = File(sandboxRoot, ".git/GITHUG_ANDROID_EDITMSG").apply {
|
val messageFile = File(sandboxRoot, ".git/GITHUG_ANDROID_EDITMSG").apply {
|
||||||
parentFile?.mkdirs()
|
parentFile?.mkdirs()
|
||||||
writeText(message)
|
writeText(message)
|
||||||
|
|||||||
@@ -8,6 +8,36 @@ object GitSandboxEngine {
|
|||||||
val quoted: Boolean = false,
|
val quoted: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun parsePatchHunkEditorInvocation(repo: RepoState, command: String): GitEditorInvocation? {
|
||||||
|
val session = repo.interactiveAddSession ?: return null
|
||||||
|
if (session.selectionAction != "patch-hunk") return null
|
||||||
|
if (command.trim().lowercase() != "e") return null
|
||||||
|
val target = session.target ?: return null
|
||||||
|
val file = repo.files.firstOrNull { it.name == target && !it.deleted } ?: return null
|
||||||
|
return GitEditorInvocation(
|
||||||
|
command = command,
|
||||||
|
kind = GitEditorCommandKind.PATCH_HUNK,
|
||||||
|
title = "Edit Patch Hunk",
|
||||||
|
initialContent = patchHunkLines(file)
|
||||||
|
.dropLastWhile { it == PatchHunkPrompt }
|
||||||
|
.joinToString("\n"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun applyPatchHunkEdit(repo: RepoState, content: String): Pair<RepoState, List<String>> {
|
||||||
|
val session = repo.interactiveAddSession ?: return repo to listOf("No patch hunk is active.")
|
||||||
|
val target = session.target ?: return repo.copy(interactiveAddSession = null) to listOf("No patch hunk is active.")
|
||||||
|
if (session.selectionAction != "patch-hunk") return repo to listOf("No patch hunk is active.")
|
||||||
|
if (content.isBlank()) return repo to listOf("Edited hunk was empty; patch was not applied.", PatchHunkPrompt)
|
||||||
|
val updatedFiles = repo.files.map { file ->
|
||||||
|
if (file.name == target && !file.deleted) file.copy(staged = true) else file
|
||||||
|
}
|
||||||
|
return repo.copy(files = updatedFiles, interactiveAddSession = null) to listOf(
|
||||||
|
"$PatchHunkPrompt e",
|
||||||
|
"Applied edited hunk.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun commandReferenceLines(): List<String> = listOf(
|
fun commandReferenceLines(): List<String> = listOf(
|
||||||
"Available sandbox commands:",
|
"Available sandbox commands:",
|
||||||
" git ",
|
" git ",
|
||||||
@@ -368,7 +398,7 @@ object GitSandboxEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"s" -> repo to listOf("$PatchHunkPrompt $answer", "Sorry, cannot split this hunk", PatchHunkPrompt)
|
"s" -> repo to listOf("$PatchHunkPrompt $answer", "Sorry, cannot split this hunk", PatchHunkPrompt)
|
||||||
"e" -> repo to listOf("$PatchHunkPrompt $answer", "Manual hunk editing is not available in this mobile sandbox.", PatchHunkPrompt)
|
"e" -> repo to listOf("$PatchHunkPrompt $answer", "Opening patch editor")
|
||||||
else -> repo to listOf("$PatchHunkPrompt $answer", "Unknown command '$answer'.", PatchHunkPrompt)
|
else -> repo to listOf("$PatchHunkPrompt $answer", "Unknown command '$answer'.", PatchHunkPrompt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -463,6 +463,34 @@ class GitSandboxEngineTest {
|
|||||||
assertTrue(selectedRepo.interactiveAddSession == null)
|
assertTrue(selectedRepo.interactiveAddSession == null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun patchAddHunkEditOpensPatchEditorInvocation() {
|
||||||
|
val repo = RepoState(initialized = true, files = listOf(GitFile("README", "A\nB\n", tracked = true)))
|
||||||
|
val (patchRepo, _) = GitSandboxEngine.execute(repo, "git add -p README")
|
||||||
|
|
||||||
|
val invocation = GitSandboxEngine.parsePatchHunkEditorInvocation(patchRepo, "e")
|
||||||
|
|
||||||
|
assertEquals(GitEditorCommandKind.PATCH_HUNK, invocation?.kind)
|
||||||
|
assertEquals("Edit Patch Hunk", invocation?.title)
|
||||||
|
assertTrue(invocation?.initialContent.orEmpty().contains("diff --git a/README b/README"))
|
||||||
|
assertTrue(invocation?.initialContent.orEmpty().contains("+A"))
|
||||||
|
assertFalse(invocation?.initialContent.orEmpty().contains("Stage this hunk"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun editedPatchHunkStagesCurrentPatchTarget() {
|
||||||
|
val repo = RepoState(initialized = true, files = listOf(GitFile("README", "A\nB\n", tracked = true)))
|
||||||
|
val (patchRepo, _) = GitSandboxEngine.execute(repo, "git add -p README")
|
||||||
|
val invocation = GitSandboxEngine.parsePatchHunkEditorInvocation(patchRepo, "e")
|
||||||
|
?: error("Expected patch editor invocation")
|
||||||
|
|
||||||
|
val (selectedRepo, output) = GitSandboxEngine.applyPatchHunkEdit(patchRepo, invocation.initialContent)
|
||||||
|
|
||||||
|
assertTrue(selectedRepo.files.single { it.name == "README" }.staged)
|
||||||
|
assertTrue(selectedRepo.interactiveAddSession == null)
|
||||||
|
assertTrue(output.any { it.contains("Applied edited hunk.") })
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun patchAddHunkDialogHandlesAdvertisedCommands() {
|
fun patchAddHunkDialogHandlesAdvertisedCommands() {
|
||||||
val commands = listOf("y", "n", "q", "a", "d", "s", "e", "p", "P", "?")
|
val commands = listOf("y", "n", "q", "a", "d", "s", "e", "p", "P", "?")
|
||||||
@@ -474,6 +502,9 @@ class GitSandboxEngineTest {
|
|||||||
|
|
||||||
assertFalse("$command should not be rejected", output.any { it.contains("Unknown command '$command'") })
|
assertFalse("$command should not be rejected", output.any { it.contains("Unknown command '$command'") })
|
||||||
assertTrue("$command should echo hunk prompt", output.any { it.contains("Stage this hunk") })
|
assertTrue("$command should echo hunk prompt", output.any { it.contains("Stage this hunk") })
|
||||||
|
if (command == "e") {
|
||||||
|
assertTrue(output.any { it.contains("Opening patch editor") })
|
||||||
|
}
|
||||||
if (command in listOf("y", "a")) {
|
if (command in listOf("y", "a")) {
|
||||||
assertTrue(updatedRepo.files.single { it.name == "README" }.staged)
|
assertTrue(updatedRepo.files.single { it.name == "README" }.staged)
|
||||||
assertTrue(updatedRepo.interactiveAddSession == null)
|
assertTrue(updatedRepo.interactiveAddSession == null)
|
||||||
@@ -539,6 +570,35 @@ class GitSandboxEngineTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun nativePatchHunkEditorSaveUpdatesGitIndex() {
|
||||||
|
val git = testGitBinary()
|
||||||
|
assumeTrue(git.exists() && git.canExecute())
|
||||||
|
val root = Files.createTempDirectory("githug-patch-edit").toFile()
|
||||||
|
try {
|
||||||
|
val runtime = GitRepositoryRuntime(root, git)
|
||||||
|
val level = level(
|
||||||
|
id = "patch-edit-test",
|
||||||
|
title = "Patch Edit Test",
|
||||||
|
description = "",
|
||||||
|
hints = emptyList(),
|
||||||
|
commandSuggestions = emptyList(),
|
||||||
|
setup = { RepoState(initialized = true, files = listOf(GitFile("README")), branches = mapOf("master" to 0)) },
|
||||||
|
validator = { _, _ -> false },
|
||||||
|
)
|
||||||
|
val repo = runtime.prepareLevel(level)
|
||||||
|
val (patchRepo, _) = runtime.execute(level, repo, "git add -p README")
|
||||||
|
val invocation = GitSandboxEngine.parsePatchHunkEditorInvocation(patchRepo, "e")
|
||||||
|
?: error("Expected patch editor invocation")
|
||||||
|
val (selectedRepo, output) = runtime.executeGitEditorCommand(level, patchRepo, invocation, invocation.initialContent)
|
||||||
|
|
||||||
|
assertTrue(output.any { it.contains("Applied edited hunk.") })
|
||||||
|
assertTrue(selectedRepo.files.single { it.name == "README" }.staged)
|
||||||
|
} finally {
|
||||||
|
root.deleteRecursively()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun nativeInteractiveRebaseUsesAppSequenceEditorContent() {
|
fun nativeInteractiveRebaseUsesAppSequenceEditorContent() {
|
||||||
val git = testGitBinary()
|
val git = testGitBinary()
|
||||||
|
|||||||
Reference in New Issue
Block a user