Split oversized runtime, sandbox, and interactive add files into focused helpers
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
package solutions.tretter.githugandroid
|
||||
|
||||
internal object InteractiveAddEngine {
|
||||
private const val PatchHunkPrompt = "(1/1) Stage this hunk [y,n,q,a,d,s,e,p,P,?]?"
|
||||
|
||||
fun start(repo: RepoState, arguments: List<String>): Pair<RepoState, List<String>> {
|
||||
val targets = arguments.filterNot { it == "-i" || it == "--interactive" || it.startsWith("--") }
|
||||
val target = targets.lastOrNull()
|
||||
val candidates = interactiveAddCandidates(repo, target)
|
||||
return repo.copy(interactiveAddSession = InteractiveAddSession(target = target)) to interactiveAddConsoleLines(candidates)
|
||||
}
|
||||
|
||||
fun startPatch(repo: RepoState, arguments: List<String>): Pair<RepoState, List<String>> {
|
||||
val targets = arguments.filterNot { it == "-p" || it == "--patch" || it.startsWith("--") }
|
||||
val target = targets.lastOrNull()
|
||||
val patchFile = interactiveAddCandidates(repo, target).firstOrNull()
|
||||
?: return repo to listOf("No changes.")
|
||||
return startPatchHunkSession(repo, patchFile.name)
|
||||
}
|
||||
|
||||
fun handleInput(repo: RepoState, input: String): Pair<RepoState, List<String>> {
|
||||
val session = repo.interactiveAddSession ?: return repo to emptyList()
|
||||
val answer = input.trim()
|
||||
if (session.selectionAction == "patch-hunk") {
|
||||
return handlePatchHunkInput(repo, session, answer)
|
||||
}
|
||||
return if (session.awaitingUpdateSelection) {
|
||||
applyInteractiveAddUpdateSelection(repo, session, answer)
|
||||
} else {
|
||||
when (answer.lowercase()) {
|
||||
"1", "s", "status" -> repo to listOf("What now> $answer") + interactiveAddConsoleLines(interactiveAddCandidates(repo, session.target))
|
||||
"2", "u", "update" -> interactiveAddSelectionPrompt(repo, session, answer, "Update>>", "update")
|
||||
"3", "r", "revert" -> interactiveAddSelectionPrompt(repo, session, answer, "Revert>>", "revert")
|
||||
"4", "a", "add untracked", "add-untracked" -> interactiveAddSelectionPrompt(repo, session, answer, "Add untracked>>", "add-untracked")
|
||||
"5", "p", "patch" -> interactiveAddSelectionPrompt(repo, session, answer, "Patch update>>", "patch")
|
||||
"6", "d", "diff" -> interactiveAddSelectionPrompt(repo, session, answer, "Diff>>", "diff")
|
||||
"7", "q", "quit" -> repo.copy(interactiveAddSession = null) to listOf("What now> $answer", "Bye.")
|
||||
"8", "h", "help" -> repo to listOf("What now> $answer") + interactiveAddConsoleLines(interactiveAddCandidates(repo, session.target))
|
||||
else -> repo to listOf("What now> $answer", "Huh ($answer)?") + interactiveAddConsoleLines(interactiveAddCandidates(repo, session.target))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.",
|
||||
)
|
||||
}
|
||||
|
||||
private fun interactiveAddSelectionPrompt(
|
||||
repo: RepoState,
|
||||
session: InteractiveAddSession,
|
||||
answer: String,
|
||||
prompt: String,
|
||||
action: String,
|
||||
): Pair<RepoState, List<String>> {
|
||||
return repo.copy(
|
||||
interactiveAddSession = session.copy(
|
||||
awaitingUpdateSelection = true,
|
||||
selectionPrompt = prompt,
|
||||
selectionAction = action,
|
||||
),
|
||||
) to listOf("What now> $answer", prompt)
|
||||
}
|
||||
|
||||
private fun applyInteractiveAddUpdateSelection(
|
||||
repo: RepoState,
|
||||
session: InteractiveAddSession,
|
||||
answer: String,
|
||||
): Pair<RepoState, List<String>> {
|
||||
val candidates = interactiveAddCandidates(repo, session.target)
|
||||
val selectedNames = selectedInteractiveAddNames(candidates, answer)
|
||||
val prompt = session.selectionPrompt
|
||||
if (selectedNames.isEmpty()) {
|
||||
return repo to listOf("$prompt $answer", "No files selected.", prompt)
|
||||
}
|
||||
|
||||
if (session.selectionAction == "patch" && selectedNames.size == 1) {
|
||||
return startPatchHunkSession(repo, selectedNames.single(), "$prompt $answer")
|
||||
}
|
||||
|
||||
val updatedFiles = applyInteractiveAddSelectionAction(repo, selectedNames, session.selectionAction)
|
||||
val updatedRepo = repo.copy(
|
||||
files = updatedFiles,
|
||||
interactiveAddSession = session.copy(
|
||||
awaitingUpdateSelection = false,
|
||||
selectionPrompt = "Update>>",
|
||||
selectionAction = "update",
|
||||
),
|
||||
)
|
||||
val summary = interactiveAddSelectionSummary(repo, updatedFiles, selectedNames, session.selectionAction)
|
||||
return updatedRepo to listOf(
|
||||
"$prompt $answer",
|
||||
summary,
|
||||
) + interactiveAddConsoleLines(interactiveAddCandidates(updatedRepo, session.target))
|
||||
}
|
||||
|
||||
private fun applyInteractiveAddSelectionAction(repo: RepoState, selectedNames: Set<String>, action: String): List<GitFile> {
|
||||
return when (action) {
|
||||
"revert" -> repo.files.mapNotNull { file ->
|
||||
if (file.name !in selectedNames || file.deleted) {
|
||||
file
|
||||
} else if (file.tracked) {
|
||||
file.copy(content = "", staged = false, deleted = false)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
"diff" -> repo.files
|
||||
else -> repo.files.map { file ->
|
||||
if (file.name in selectedNames && !file.deleted) file.copy(staged = true) else file
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun interactiveAddSelectionSummary(
|
||||
repo: RepoState,
|
||||
updatedFiles: List<GitFile>,
|
||||
selectedNames: Set<String>,
|
||||
action: String,
|
||||
): String {
|
||||
return when (action) {
|
||||
"revert" -> "reverted ${selectedNames.size} path(s)"
|
||||
"diff" -> selectedNames.joinToString("\n") { "diff -- $it" }
|
||||
else -> {
|
||||
val stagedCount = updatedFiles.count { updatedFile ->
|
||||
val before = repo.files.firstOrNull { it.name == updatedFile.name }
|
||||
updatedFile.staged && before?.staged != true
|
||||
}
|
||||
"updated $stagedCount path(s)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startPatchHunkSession(repo: RepoState, target: String, prefixLine: String? = null): Pair<RepoState, List<String>> {
|
||||
val file = repo.files.firstOrNull { it.name == target && !it.deleted }
|
||||
?: return repo to listOfNotNull(prefixLine, "No changes.")
|
||||
val session = InteractiveAddSession(
|
||||
target = target,
|
||||
awaitingUpdateSelection = false,
|
||||
selectionPrompt = PatchHunkPrompt,
|
||||
selectionAction = "patch-hunk",
|
||||
)
|
||||
val output = listOfNotNull(prefixLine) + patchHunkLines(file)
|
||||
return repo.copy(interactiveAddSession = session) to output
|
||||
}
|
||||
|
||||
private fun handlePatchHunkInput(repo: RepoState, session: InteractiveAddSession, answer: String): Pair<RepoState, List<String>> {
|
||||
val target = session.target ?: return repo.copy(interactiveAddSession = null) to listOf("No changes.")
|
||||
return when (answer.lowercase()) {
|
||||
"y", "a" -> {
|
||||
val updatedFiles = repo.files.map { file ->
|
||||
if (file.name == target && !file.deleted) file.copy(staged = true) else file
|
||||
}
|
||||
repo.copy(files = updatedFiles, interactiveAddSession = null) to listOf("$PatchHunkPrompt $answer")
|
||||
}
|
||||
"n", "d" -> repo.copy(interactiveAddSession = null) to listOf("$PatchHunkPrompt $answer")
|
||||
"q" -> repo.copy(interactiveAddSession = null) to listOf("$PatchHunkPrompt $answer", "Quit")
|
||||
"?" -> repo to listOf(
|
||||
"$PatchHunkPrompt $answer",
|
||||
"y - stage this hunk",
|
||||
"n - do not stage this hunk",
|
||||
"q - quit; do not stage this hunk or any remaining ones",
|
||||
"a - stage this hunk and all later hunks in the file",
|
||||
"d - do not stage this hunk or any later hunks in the file",
|
||||
"s - split the current hunk into smaller hunks",
|
||||
"e - manually edit the current hunk",
|
||||
"p - print the current hunk",
|
||||
"? - print help",
|
||||
PatchHunkPrompt,
|
||||
)
|
||||
"p" -> {
|
||||
val file = repo.files.firstOrNull { it.name == target && !it.deleted }
|
||||
if (file == null) {
|
||||
repo.copy(interactiveAddSession = null) to listOf("No changes.")
|
||||
} else {
|
||||
repo to listOf("$PatchHunkPrompt $answer") + patchHunkLines(file)
|
||||
}
|
||||
}
|
||||
"s" -> repo to listOf("$PatchHunkPrompt $answer", "Sorry, cannot split this hunk", PatchHunkPrompt)
|
||||
"e" -> repo to listOf("$PatchHunkPrompt $answer", "Opening patch editor")
|
||||
else -> repo to listOf("$PatchHunkPrompt $answer", "Unknown command '$answer'.", PatchHunkPrompt)
|
||||
}
|
||||
}
|
||||
|
||||
private fun patchHunkLines(file: GitFile): List<String> {
|
||||
val lines = file.content.lines()
|
||||
val nonEmptyLines = lines.dropLastWhile { it.isEmpty() }
|
||||
val addedCount = nonEmptyLines.size.coerceAtLeast(1)
|
||||
return buildList {
|
||||
add("diff --git a/${file.name} b/${file.name}")
|
||||
add("index 0000000..0000001 100644")
|
||||
add("--- a/${file.name}")
|
||||
add("+++ b/${file.name}")
|
||||
add("@@ -1 +1,$addedCount @@")
|
||||
if (nonEmptyLines.isEmpty()) {
|
||||
add("+")
|
||||
} else {
|
||||
nonEmptyLines.forEach { line -> add("+$line") }
|
||||
}
|
||||
add(PatchHunkPrompt)
|
||||
}
|
||||
}
|
||||
|
||||
private fun interactiveAddConsoleLines(candidates: List<GitFile>): List<String> {
|
||||
return buildList {
|
||||
add(" staged unstaged path")
|
||||
candidates.forEachIndexed { index, file ->
|
||||
val staged = if (file.staged) "unchanged" else "+0/-0"
|
||||
val unstaged = when {
|
||||
file.tracked -> "+1/-0"
|
||||
else -> "+0/-0"
|
||||
}
|
||||
add("${index + 1}: ${staged.padEnd(10)} ${unstaged.padEnd(9)} ${file.name}")
|
||||
}
|
||||
if (candidates.isEmpty()) {
|
||||
add("No changes.")
|
||||
}
|
||||
add("*** Commands ***")
|
||||
add(" 1: status 2: update 3: revert 4: add untracked")
|
||||
add(" 5: patch 6: diff 7: quit 8: help")
|
||||
add("What now>")
|
||||
}
|
||||
}
|
||||
|
||||
private fun interactiveAddCandidates(repo: RepoState, target: String?): List<GitFile> {
|
||||
return repo.files.filter { file ->
|
||||
!file.deleted && (target == null || target == "." || file.name == target || file.name.startsWith(target.trimEnd('/') + "/"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectedInteractiveAddNames(candidates: List<GitFile>, answer: String): Set<String> {
|
||||
if (answer == "*") return candidates.map { it.name }.toSet()
|
||||
return answer.split(Regex("[,\\s]+"))
|
||||
.mapNotNull { token ->
|
||||
token.toIntOrNull()
|
||||
?.takeIf { it in 1..candidates.size }
|
||||
?.let { candidates[it - 1].name }
|
||||
}
|
||||
.toSet()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user