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/Exercise.kt
app/src/main/java/solutions/tretter/githugandroid/GameModels.kt
app/src/main/java/solutions/tretter/githugandroid/GitHugApp.kt
app/src/main/java/solutions/tretter/githugandroid/GitHugTheme.kt
app/src/main/java/solutions/tretter/githugandroid/GitRuntime.kt
app/src/main/java/solutions/tretter/githugandroid/Header.kt
app/src/main/java/solutions/tretter/githugandroid/Levels.kt
app/src/main/java/solutions/tretter/githugandroid/MainActivity.kt
app/src/main/java/solutions/tretter/githugandroid/PaneLayoutLogic.kt
app/src/main/java/solutions/tretter/githugandroid/PaneLayoutModels.kt
app/src/main/java/solutions/tretter/githugandroid/PaneLayoutStore.kt
app/src/main/java/solutions/tretter/githugandroid/Panes.kt
app/src/main/java/solutions/tretter/githugandroid/Terminal.kt
app/src/main/java/solutions/tretter/githugandroid/Visual.kt
app/src/main/java/solutions/tretter/githugandroid/levels/AdvancedLevels.kt
app/src/main/java/solutions/tretter/githugandroid/levels/CoreLevels.kt
app/src/main/java/solutions/tretter/githugandroid/levels/LevelCatalog.kt
This commit is contained in:
Joe Tretter
2026-04-24 12:40:30 -05:00
parent f4f24c9557
commit 8bd3b4d913
18 changed files with 21 additions and 21 deletions

View File

@@ -0,0 +1,258 @@
package solutions.tretter.githugandroid
import android.os.Build
import android.content.Context
import java.io.File
class GitRepositoryRuntime(private val context: Context) {
private val sandboxesRoot = File(context.filesDir, "githug-sandboxes")
private val extractedGitDir = File(context.filesDir, "native-git/bin")
fun startupBanner(): String {
return if (nativeGitBinary() != null) {
"Welcome to GitHug Android. Native Git prototype ready."
} else {
"Welcome to GitHug Android. Native Git binary not bundled for this ABI yet; using in-memory fallback."
}
}
fun prepareLevel(level: Level): RepoState {
val nativeGit = nativeGitBinary()
if (nativeGit == null) {
return level.setup()
}
val sandbox = sandboxDir(level)
sandbox.deleteRecursively()
sandbox.mkdirs()
val desired = level.setup()
desired.files.forEach { file ->
File(sandbox, file.name).apply {
parentFile?.mkdirs()
writeText(file.content)
}
}
val needsGit = desired.initialized || desired.files.any { it.staged || it.tracked } || desired.commits.isNotEmpty()
if (needsGit) {
val initResult = runGit(nativeGit, sandbox, listOf("init", "-b", desired.headBranch))
if (initResult.exitCode != 0) {
runGit(nativeGit, sandbox, listOf("init"))
runGit(nativeGit, sandbox, listOf("checkout", "-B", desired.headBranch))
}
val stageTargets = desired.files.filter { it.staged || it.tracked }.map { it.name }
if (stageTargets.isNotEmpty()) {
runGit(nativeGit, sandbox, listOf("add") + stageTargets)
}
}
return inspectSandbox(level)
}
fun execute(level: Level, currentRepo: RepoState, command: String): Pair<RepoState, List<String>> {
val nativeGit = nativeGitBinary()
if (nativeGit == null) {
return GitSandboxEngine.execute(currentRepo, command)
}
val sandbox = sandboxDir(level)
if (!sandbox.exists()) {
prepareLevel(level)
}
val sandboxRoot = sandbox.canonicalFile
val workingDir = File(sandboxRoot, currentRepo.currentDir).canonicalFile
.takeIf { it.path.startsWith(sandboxRoot.path) } ?: sandboxRoot
val tokens = GitSandboxEngine.tokenizeCommand(command)
if (tokens.isEmpty()) return inspectSandbox(level).copy(currentDir = currentRepo.currentDir) to emptyList()
val result = when (tokens.first()) {
"git" -> currentRepo to runGit(nativeGit, workingDir, tokens.drop(1)).outputLines
"help", "?" -> currentRepo to commandReferenceLines()
else -> executeHelperCommand(sandboxRoot, workingDir, currentRepo, tokens)
}
return inspectSandbox(level).copy(currentDir = result.first.currentDir) to result.second
}
fun commandReferenceLines(): List<String> {
return buildList {
addAll(GitSandboxEngine.commandReferenceLines())
add("Native Git prototype:")
add(" binary path: nativeLibraryDir/libgit.so")
add(" selected ABI: ${Build.SUPPORTED_ABIS.firstOrNull() ?: "unknown"}")
add(" helper commands: ls, pwd, cat, touch, mkdir, rm, echo")
}
}
private fun executeHelperCommand(sandboxRoot: File, workingDir: File, currentRepo: RepoState, tokens: List<String>): Pair<RepoState, List<String>> {
return when (tokens.first()) {
"ls" -> currentRepo to workingDir.listFiles()
?.filterNot { it.name == ".git" }
?.sortedBy { it.name }
?.map { it.name }
.orEmpty()
"pwd" -> currentRepo to listOf(
"/sandbox" + if (workingDir == sandboxRoot) "" else "/${workingDir.relativeTo(sandboxRoot).path}"
)
"cat" -> {
val target = tokens.getOrNull(1) ?: return currentRepo to listOf("usage: cat <file>")
val file = File(workingDir, target)
if (!file.exists() || file.isDirectory) currentRepo to listOf("cat: $target: No such file")
else currentRepo to file.readLines().ifEmpty { listOf("") }
}
"touch" -> {
val target = tokens.getOrNull(1) ?: return currentRepo to listOf("usage: touch <file>")
val file = File(workingDir, target)
if (file.exists()) {
currentRepo to listOf("$target already exists")
} else {
file.parentFile?.mkdirs()
file.writeText("")
currentRepo to emptyList()
}
}
"mkdir" -> {
val target = tokens.getOrNull(1) ?: return currentRepo to listOf("usage: mkdir <dir>")
val dir = File(workingDir, target)
if (dir.exists()) currentRepo to listOf("mkdir: $target: File exists") else {
dir.mkdirs()
currentRepo to emptyList()
}
}
"cd" -> {
val target = tokens.getOrNull(1) ?: return currentRepo to listOf("usage: cd <dir>")
val dir = File(workingDir, target).canonicalFile
when {
!dir.exists() -> currentRepo to listOf("cd: $target: does not exist")
!dir.isDirectory -> currentRepo to listOf("cd: $target: Not a directory")
!dir.path.startsWith(sandboxRoot.path) -> currentRepo to listOf("cd: $target: Permission denied")
else -> {
val relativeDir = sandboxRoot.toPath().relativize(dir.toPath()).toString().ifEmpty { "." }
currentRepo.copy(currentDir = relativeDir) to emptyList()
}
}
}
"rm" -> {
val target = tokens.getOrNull(1) ?: return currentRepo to listOf("usage: rm <path>")
val file = File(workingDir, target)
if (!file.exists()) currentRepo to listOf("rm: $target: No such file or directory") else {
file.deleteRecursively()
currentRepo to emptyList()
}
}
"echo" -> currentRepo to listOf(tokens.drop(1).joinToString(" "))
else -> currentRepo to listOf("Command not supported in prototype runtime. Try a git command or helper command.")
}
}
private fun inspectSandbox(level: Level): RepoState {
val sandbox = sandboxDir(level)
val nativeGit = nativeGitBinary()
val filesOnDisk = sandbox.listFiles()
?.filter { it.isFile && it.name != ".git" }
.orEmpty()
if (nativeGit == null || !File(sandbox, ".git").exists()) {
return RepoState(
initialized = File(sandbox, ".git").exists(),
files = filesOnDisk.map { GitFile(name = it.name, content = it.readText()) },
)
}
val statusResult = runGit(nativeGit, sandbox, listOf("status", "--porcelain"))
val statusMap = mutableMapOf<String, Pair<Boolean, Boolean>>()
statusResult.outputLines.forEach { line ->
if (line.length < 4) return@forEach
val x = line[0]
val y = line[1]
val path = line.substring(3).trim()
val staged = x != ' ' && x != '?'
val tracked = x != '?' || y != '?'
statusMap[path] = staged to tracked
}
val logResult = runGit(nativeGit, sandbox, listOf("log", "--pretty=format:%h\t%s"))
val branchResult = runGit(nativeGit, sandbox, listOf("branch", "--list"))
val tagResult = runGit(nativeGit, sandbox, listOf("tag", "--list"))
val remoteResult = runGit(nativeGit, sandbox, listOf("remote", "-v"))
val headResult = runGit(nativeGit, sandbox, listOf("branch", "--show-current"))
val commits = if (logResult.exitCode == 0) {
logResult.outputLines.filter { it.isNotBlank() }.mapNotNull { line ->
val parts = line.split('\t', limit = 2)
if (parts.isEmpty()) null else CommitNode(parts[0], parts.getOrElse(1) { "" })
}
} else {
emptyList()
}
return RepoState(
initialized = true,
files = filesOnDisk.map { file ->
val (staged, tracked) = statusMap[file.name] ?: (false to true)
GitFile(
name = file.name,
content = file.readText(),
staged = staged,
tracked = tracked,
)
},
commits = commits,
headBranch = headResult.outputLines.firstOrNull()?.ifBlank { null } ?: "master",
branches = branchResult.outputLines.map { it.removePrefix("*").trim() }.filter { it.isNotBlank() }.associateWith { 0 },
tags = tagResult.outputLines.filter { it.isNotBlank() },
remotes = remoteResult.outputLines.mapNotNull { line ->
val parts = line.trim().split(Regex("\\s+"))
if (parts.size >= 2) parts[0] to parts[1] else null
}.toMap(),
)
}
private fun nativeGitBinary(): File? {
return packagedNativeGitBinary()
}
private fun packagedNativeGitBinary(): File? {
val nativeLibraryDir = context.applicationInfo.nativeLibraryDir ?: return null
val candidate = File(nativeLibraryDir, "libgit.so")
return candidate.takeIf { it.exists() && it.canExecute() }
}
private fun sandboxDir(level: Level): File = File(sandboxesRoot, level.id)
private fun runGit(binary: File, workingDir: File, arguments: List<String>): ProcessExecutionResult {
return runProcess(binary, workingDir, arguments)
}
private fun runProcess(binary: File, workingDir: File, arguments: List<String>): ProcessExecutionResult {
return try {
val process = ProcessBuilder(listOf(binary.absolutePath) + arguments)
.directory(workingDir)
.redirectErrorStream(true)
.apply {
environment()["HOME"] = workingDir.absolutePath
environment()["GIT_CONFIG_NOSYSTEM"] = "1"
environment()["GIT_AUTHOR_NAME"] = "GitHug"
environment()["GIT_AUTHOR_EMAIL"] = "githug@example.com"
environment()["GIT_COMMITTER_NAME"] = "GitHug"
environment()["GIT_COMMITTER_EMAIL"] = "githug@example.com"
environment()["LC_ALL"] = "C"
}
.start()
val output = process.inputStream.bufferedReader().readLines()
val exit = process.waitFor()
ProcessExecutionResult(exitCode = exit, outputLines = output.ifEmpty { if (exit == 0) emptyList() else listOf("Command failed") })
} catch (error: Exception) {
ProcessExecutionResult(exitCode = -1, outputLines = listOf("Native Git execution failed: ${error.message ?: error::class.java.simpleName}"))
}
}
}
private data class ProcessExecutionResult(
val exitCode: Int,
val outputLines: List<String>,
)