Start patch dialog for git add -p
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
package solutions.tretter.githugandroid
|
||||
|
||||
object GitSandboxEngine {
|
||||
private const val PatchHunkPrompt = "(1/1) Stage this hunk [y,n,q,a,d,s,e,p,P,?]?"
|
||||
|
||||
data class ShellToken(
|
||||
val value: String,
|
||||
val quoted: Boolean = false,
|
||||
@@ -108,6 +110,9 @@ object GitSandboxEngine {
|
||||
if (parts.drop(2).any { it == "-i" || it == "--interactive" }) {
|
||||
return interactiveAdd(repo, parts.drop(2))
|
||||
}
|
||||
if (parts.drop(2).any { it == "-p" || it == "--patch" }) {
|
||||
return interactiveAddPatch(repo, parts.drop(2))
|
||||
}
|
||||
val target = parts.drop(2).lastOrNull { !it.startsWith("-") }
|
||||
?: return repo to listOf("usage: git add <path>")
|
||||
if (target != "." && repo.files.none { it.name == target && !it.deleted }) {
|
||||
@@ -180,6 +185,14 @@ object GitSandboxEngine {
|
||||
return repo.copy(interactiveAddSession = InteractiveAddSession(target = target)) to interactiveAddConsoleLines(candidates)
|
||||
}
|
||||
|
||||
private fun interactiveAddPatch(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)
|
||||
}
|
||||
|
||||
private fun interactiveAddConsoleLines(candidates: List<GitFile>): List<String> {
|
||||
return buildList {
|
||||
add(" staged unstaged path")
|
||||
@@ -204,6 +217,9 @@ object GitSandboxEngine {
|
||||
private fun handleInteractiveAddInput(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 {
|
||||
@@ -249,6 +265,10 @@ object GitSandboxEngine {
|
||||
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,
|
||||
@@ -302,6 +322,76 @@ object GitSandboxEngine {
|
||||
}
|
||||
}
|
||||
|
||||
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", "Manual hunk editing is not available in this mobile sandbox.", PatchHunkPrompt)
|
||||
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 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('/') + "/"))
|
||||
|
||||
@@ -14,9 +14,16 @@ internal fun stageLinesLevel(): Level = level(
|
||||
hints = listOf("Read about the flags which can be passed to the `add` command."),
|
||||
commandSuggestions = listOf("git add -p feature.rb"),
|
||||
setup = { RepoState(initialized = true, files = listOf(GitFile("feature.rb", "this is the class of my feature\nThis change belongs to the first feature\nThis change belongs to the second feature", tracked = true)), branches = mapOf("master" to 1)) },
|
||||
nativeSetup = {
|
||||
resetFiles()
|
||||
write("feature.rb", "this is the class of my feature\n")
|
||||
addCommit("Initial feature", "feature.rb")
|
||||
write("feature.rb", "this is the class of my feature\nThis change belongs to the first feature\nThis change belongs to the second feature")
|
||||
true
|
||||
},
|
||||
validator = repoPredicate { repo -> repo.files.any { it.name == "feature.rb" && it.staged } },
|
||||
testCases = listOf(
|
||||
levelTestCase("patch add feature file", "git add -p feature.rb"),
|
||||
levelTestCase("patch long option", "git add --patch feature.rb"),
|
||||
levelTestCase("patch add feature file", "git add -p feature.rb", "y"),
|
||||
levelTestCase("patch long option", "git add --patch feature.rb", "y"),
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user