Auto-commit after successful build: update app gameplay/UI, improve build setup

Changed files:\napp/build.gradle.kts
app/src/main/java/solutions/tretter/githugandroid/GitHelpCommands.kt
app/src/main/java/solutions/tretter/githugandroid/GitHugApp.kt
app/src/main/java/solutions/tretter/githugandroid/GitRuntime.kt
app/src/main/java/solutions/tretter/githugandroid/ManPageDialog.kt
app/src/test/java/solutions/tretter/githugandroid/GitHelpCommandsTest.kt
This commit is contained in:
Joe Tretter
2026-05-02 22:09:20 -05:00
parent dcd5589d74
commit ca20b0a358
6 changed files with 247 additions and 2 deletions

View File

@@ -19,8 +19,8 @@ android {
applicationId = "solutions.tretter.githugandroid" applicationId = "solutions.tretter.githugandroid"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 98 versionCode = 99
versionName = "0.1.97" versionName = "0.1.98"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true

View File

@@ -0,0 +1,13 @@
package solutions.tretter.githugandroid
data class GitHelpInvocation(
val topic: String,
val command: String,
)
fun parseGitHelpInvocation(command: String): GitHelpInvocation? {
val tokens = GitSandboxEngine.tokenizeCommand(command)
if (tokens.size < 3 || tokens[0] != "git" || tokens[1] != "help") return null
val topic = tokens[2].takeIf { it.matches(Regex("[A-Za-z0-9_-]+")) } ?: return null
return GitHelpInvocation(topic = topic, command = command)
}

View File

@@ -61,6 +61,7 @@ fun GitHugApp() {
var historyDraft by remember { mutableStateOf("") } var historyDraft by remember { mutableStateOf("") }
var hasRestoredProgress by remember { mutableStateOf(false) } var hasRestoredProgress by remember { mutableStateOf(false) }
var editorState by remember { mutableStateOf<TextEditorState?>(null) } var editorState by remember { mutableStateOf<TextEditorState?>(null) }
var manPageState by remember { mutableStateOf<ManPageState?>(null) }
val currentLevel = levels[currentLevelIndex] val currentLevel = levels[currentLevelIndex]
LaunchedEffect(persistedPaneLayout) { LaunchedEffect(persistedPaneLayout) {
@@ -168,6 +169,7 @@ fun GitHugApp() {
historyIndex = -1 historyIndex = -1
historyDraft = "" historyDraft = ""
editorState = null editorState = null
manPageState = null
applyRecommendedPaneWeights(persist = false) applyRecommendedPaneWeights(persist = false)
} }
@@ -184,6 +186,7 @@ fun GitHugApp() {
historyIndex = -1 historyIndex = -1
historyDraft = "" historyDraft = ""
editorState = null editorState = null
manPageState = null
paneLayout = paneLayout.copy( paneLayout = paneLayout.copy(
weights = paneLayout.weights + recommendedPaneWeights( weights = paneLayout.weights + recommendedPaneWeights(
heights = recommendedPaneHeights( heights = recommendedPaneHeights(
@@ -323,6 +326,17 @@ fun GitHugApp() {
applyRecommendedPaneWeights(persist = false) applyRecommendedPaneWeights(persist = false)
} }
fun openManPage(invocation: GitHelpInvocation) {
val content = runtime.gitManPage(currentLevel, repo, invocation.topic)
output = buildList {
addAll(output)
add("$ ${invocation.command}")
add("Opened git help viewer for ${invocation.topic}")
}
manPageState = ManPageState(topic = invocation.topic, content = content)
applyRecommendedPaneWeights(persist = false)
}
fun saveEditor() { fun saveEditor() {
val state = editorState ?: return val state = editorState ?: return
val targetPath = state.saveAsPath.trim() val targetPath = state.saveAsPath.trim()
@@ -352,6 +366,12 @@ fun GitHugApp() {
return return
} }
val helpInvocation = parseGitHelpInvocation(raw)
if (helpInvocation != null) {
openManPage(helpInvocation)
return
}
val (newRepo, lines) = runtime.execute(currentLevel, repo, raw) val (newRepo, lines) = runtime.execute(currentLevel, repo, raw)
applyCommandResult(raw, newRepo, lines, echoCommand = true) applyCommandResult(raw, newRepo, lines, echoCommand = true)
} }
@@ -460,5 +480,12 @@ fun GitHugApp() {
onSave = { saveEditor() }, onSave = { saveEditor() },
) )
} }
manPageState?.let { state ->
ManPageDialog(
state = state,
onClose = { manPageState = null },
)
}
} }
} }

View File

@@ -85,9 +85,26 @@ class GitRepositoryRuntime(private val context: Context) {
add(" selected ABI: ${Build.SUPPORTED_ABIS.firstOrNull() ?: "unknown"}") add(" selected ABI: ${Build.SUPPORTED_ABIS.firstOrNull() ?: "unknown"}")
add(" helper commands: ls, pwd, cat, touch, mkdir, rm, echo") add(" helper commands: ls, pwd, cat, touch, mkdir, rm, echo")
add(" visual editors: vi, nano, emacs, ed, ex") add(" visual editors: vi, nano, emacs, ed, ex")
add(" git help <command>")
} }
} }
fun gitManPage(level: Level, currentRepo: RepoState, topic: String): String {
val nativeGit = nativeGitBinary()
if (nativeGit == null) {
return fallbackManPage(topic)
}
val sandboxRoot = sandboxDir(level).canonicalFile
val workingDir = File(sandboxRoot, currentRepo.currentDir).canonicalFile
.takeIf { it.path.startsWith(sandboxRoot.path) } ?: sandboxRoot
val result = runGit(nativeGit, workingDir, listOf(topic, "-h"))
return result.outputLines
.filterNot { it.contains("no man viewer", ignoreCase = true) }
.joinToString("\n")
.ifBlank { fallbackManPage(topic) }
}
fun readEditorFile(level: Level, currentRepo: RepoState, path: String): Pair<String, List<String>> { fun readEditorFile(level: Level, currentRepo: RepoState, path: String): Pair<String, List<String>> {
val nativeGit = nativeGitBinary() val nativeGit = nativeGitBinary()
if (nativeGit == null) { if (nativeGit == null) {
@@ -198,6 +215,80 @@ class GitRepositoryRuntime(private val context: Context) {
} }
} }
private fun fallbackManPage(topic: String): String {
val body = when (topic) {
"tag" -> """
NAME
git-tag - Create, list, delete or verify a tag object
SYNOPSIS
git tag <tagname>
git tag -d <tagname>
git tag
DESCRIPTION
Tags name specific points in history. In GitHug Android,
tag levels usually expect you to create or reference a tag
by name.
EXAMPLES
git tag new_tag
git tag v1.2
""".trimIndent()
"branch" -> """
NAME
git-branch - List, create, or delete branches
SYNOPSIS
git branch <branchname>
git branch -d <branchname>
EXAMPLES
git branch test_code
git branch -d delete_me
""".trimIndent()
"checkout" -> """
NAME
git-checkout - Switch branches or restore files
SYNOPSIS
git checkout <branch>
git checkout -b <branch>
git checkout -- <file>
EXAMPLES
git checkout -b my_branch
git checkout -- config.rb
""".trimIndent()
"add" -> """
NAME
git-add - Add file contents to the index
SYNOPSIS
git add <path>
git add .
git add -p <path>
""".trimIndent()
"commit" -> """
NAME
git-commit - Record changes to the repository
SYNOPSIS
git commit -m <message>
git commit --amend
""".trimIndent()
else -> """
NAME
git-$topic
DESCRIPTION
No bundled fallback manpage is available for git $topic.
Try the command suggestions or use git help for another command.
""".trimIndent()
}
return body
}
private fun materializeNativeGitState(nativeGit: File, sandbox: File, desired: RepoState) { private fun materializeNativeGitState(nativeGit: File, sandbox: File, desired: RepoState) {
desired.config.forEach { (key, value) -> desired.config.forEach { (key, value) ->
runGit(nativeGit, sandbox, listOf("config", key, value)) runGit(nativeGit, sandbox, listOf("config", key, value))

View File

@@ -0,0 +1,89 @@
package solutions.tretter.githugandroid
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
data class ManPageState(
val topic: String,
val content: String,
)
@Composable
fun ManPageDialog(
state: ManPageState,
onClose: () -> Unit,
) {
val scrollState = rememberScrollState()
Dialog(onDismissRequest = onClose) {
Surface(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.86f),
color = PanelPrimary,
shape = RoundedCornerShape(8.dp),
) {
Column(
modifier = Modifier.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
Text(
text = "Git Manpage",
color = TextPrimary,
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Button(
onClick = onClose,
colors = ButtonDefaults.buttonColors(
containerColor = Accent,
contentColor = AppBackground,
),
) {
Text("Close")
}
}
Text(
text = "git ${state.topic}",
color = TextPrimary,
fontWeight = FontWeight.Bold,
)
Text(
text = state.content,
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.background(TerminalBackground, RoundedCornerShape(6.dp))
.padding(10.dp)
.verticalScroll(scrollState),
color = TextPrimary,
fontFamily = FontFamily.Monospace,
fontSize = 13.sp,
)
}
}
}
}

View File

@@ -0,0 +1,25 @@
package solutions.tretter.githugandroid
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
class GitHelpCommandsTest {
@Test
fun parsesGitHelpCommand() {
val invocation = parseGitHelpInvocation("git help tag")
assertEquals("tag", invocation?.topic)
assertEquals("git help tag", invocation?.command)
}
@Test
fun ignoresNonHelpCommands() {
assertNull(parseGitHelpInvocation("git tag -h"))
}
@Test
fun rejectsUnsafeHelpTopic() {
assertNull(parseGitHelpInvocation("git help ../config"))
}
}